SpringBoot+Vue儿童疫苗接种管理系统设计与实现解析

SpringBoot+Vue儿童疫苗接种管理系统设计与实现解析 去年一个做医疗信息化的朋友找我聊项目说院区想上一套儿童疫苗接种管理系统核心诉求是家长能在线预约、门诊能扫码接种、疫苗批号统一追溯、库存和效期不能出半点差错。那段时间我刚完成一个基于SpringBootVue的疫苗接种全流程管理系统,正好把整个思路重新梳理了一遍。这篇文章就围绕“springboot医院儿童疫苗接种管理系统”这个项目把从需求拆解、表结构设计、后端接口实现到Vue3前端页面落地、最终打包部署的完整过程写出来。业务上最核心的其实不是增删改查而是疫苗的“应种月龄计算、批次追溯、库存防超卖、接种三查七对”这类细节。适合正在做类似管理系统、打算用这个方向做毕业设计或者刚入手前后端分离项目的同学参考。1. 项目全貌与设计思路拆解1.1 系统定位与核心业务主线儿童疫苗接种管理系统本质上是给医院预防接种门诊用的一套业务管理系统同时又要给家长提供预约入口。它不是简单的“儿童信息管理”业务主线非常清晰家长/监护人先建儿童档案系统根据儿童的出生日期和本地免疫规划程序自动算出当前应该接种哪几剂次疫苗家长选择时间段预约门诊医生在接种台扫描儿童档案编号核对疫苗品类、批号、剂量、有效期接种完成后写入接种记录同时扣减该批次疫苗库存如果出现不良反应还要单独上报登记。这套流程里有几个关键角色权限边界天然不同角色核心操作关注点家长/监护人建档、在线预约、查看接种记录操作简单、预约方便接种医生核对信息、录入接种记录、处理不良反应信息准确、流程严谨疫苗管理员入库、批次管理、库存预警效期不可错、库存不可负系统管理员用户权限、基础数据、报表统计权限分明、数据可追溯在设计系统时我最大的体会是不要把“儿童档案”和“居民健康档案”混为一谈。儿童疫苗接种档案的核心字段是出生日期、监护人信息、既往过敏史以及各剂次接种状态。这个档案在系统里是后续所有业务的基础所以主键、唯一约束和逻辑删除都要在设计阶段想清楚。1.2 技术选型为什么锁定SpringBootVue项目选型的时候医院那边给的条件是技术栈最好成熟稳定、团队能接得住、不要引入太重的中间件。最终定下来的是SpringBoot Vue前后端分离配合MySQL、Redis、MyBatis-Plus这套组合。先说SpringBoot版本。做这类管理系统我强烈建议用Spring Boot 2.7.x而不是无脑上3.x。原因很直接很多医院内网环境还是JDK8而Spring Boot 3要求JDK17起步并且javax.servlet的包名改成了jakarta.servlet老项目迁移要踩不少坑。如果你只是自己学习或做毕业设计用3.x倒无所谓但如果是真实项目交付稳定优先选2.7.18即可。后端ORM选了MyBatis-Plus而不是JPA主要看重两点一是分页插件开箱即用十张表以内的常规CRUD基本不用手写SQL二是复杂统计报表可以自己写XML里的SQL不会被框架限制。JPA在小项目里上手也快但一旦遇到多表联查、复杂条件筛选写起来并不比MyBatis省心。前端这一侧我选的是Vue3 Vite Element Plus Pinia Vue Router。Vite相比webpack启动速度快得多开发体验好Vue3组合式API在页面逻辑复杂时比Vue2的Options API更清晰。Element Plus的后台管理组件很齐全表格、表单、日期选择器、上传组件这些直接复用能省下大量开发时间。至于Redis我这里主要用来做验证码存储和预约限流的计数。如果你部署环境实在没有Redis也可以用JWT无状态方案扛过去但预约时段防超卖那部分最好还是靠数据库乐观锁来保证这个后面详细说。1.3 数据库表设计与核心字段规划儿童疫苗接种管理系统的表结构基本可以分成四组档案组、疫苗组、业务组、权限组。下面把核心表罗列一下儿童档案表 childid、儿童姓名、性别、出生日期、监护人姓名、监护人手机号、证件类型、证件号、过敏史、建档时间。唯一约束通常是“证件类型证件号”如果医院内部有儿童编号也可以用儿童编号做唯一键。疫苗表 vaccineid、疫苗名称、疫苗简称、剂次序号、适用月龄、疫苗类型一类/二类、规格、生产厂家。注意正常应该维护“疫苗与剂次”的对应关系比如乙肝疫苗第1剂、第2剂、第3剂分别是三条数据。疫苗批次表 vaccine_batchid、疫苗id、批号、生产日期、有效期至、库存数量、锁定数量、状态正常/预警/过期/停用。这个表是整个库存追溯的核心。预约表 appointmentid、儿童id、疫苗批次id、预约日期、预约时段、预约状态待接种/已完成/已取消/爽约、预约码。接种记录表 inoculation_recordid、儿童id、预约id、疫苗id、疫苗批次id、接种日期、接种部位、接种剂量、接种医生、不良反应备注。不良反应表 adverse_reactionid、儿童id、接种记录id、发生时间、反应描述、严重程度、处理方式。用户权限表sys_user、sys_role、sys_user_role、sys_menu、sys_role_menu。设计时几个容易被忽略的点第一儿童档案要有“逻辑删除”字段但接种记录绝对不能物理删除。疫苗记录属于医疗留痕数据只能作废并标记原因不能直接从库里抹掉。第二预约表最好冗余一份“疫苗批次id”这样既能锁定库存接种时核验也方便。但要注意预约和批次关联后批次过期或停用时预约单处理逻辑要跟上。第三接种记录表建议冗余“疫苗名称”和“批号”字段。虽然这样违反了一点数据库范式但考虑到防接种台网络抖动、跨表查询性能以及后续打印接种凭证的便利性这个冗余非常值得。2. 后端核心模块设计与实现2.1 认证授权与统一响应体设计这类对内管理系统我没有直接用Spring Security而是采用轻量级JWT 拦截器方案。原因很简单项目菜单和按钮权限是自己维护的不涉及OAuth2、SSO这些复杂场景用Spring Security反而增加了配置和学习成本。登录流程是用户提交账号密码后端校验通过后生成JWT tokentoken里放userId然后让前端每次请求都放到Authorization请求头。后端写一个拦截器JwtInterceptor在preHandle里解析token、校验有效性再把用户信息set到ThreadLocal里后续代码随时可以取到当前用户。// 登录接口核心逻辑 public ResultLoginVO login(RequestBody LoginDTO dto) { SysUser user userService.login(dto.getUsername(), dto.getPassword()); if (user null) { return Result.error(账号或密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername()); return Result.success(new LoginVO(token, user.getUsername())); }// JWT拦截器核心代码示意 public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录或登录已过期); } // 解析token失败则抛异常 Long userId JwtUtil.getUserId(token); UserContext.set(userId); return true; } }统一响应体我定义成Result类code、message、data三个字段。业务正常code为200权限不足为403未登录为401其余业务错误根据场景定义。再配合全局异常处理器RestControllerAdvice所有异常都会被转成统一JSON返回前端就很省事。这里有个实际开发中很容易踩的坑token过期之后前端如果直接跳登录页会打断正在填写的表单。我这边处理方式是响应拦截器判断到code为401先弹提示同时清掉本地token并跳登录页如果正在上传文件或者提交数据提醒用户先保存再重新登录避免数据丢失。2.2 接种计划与预约时段控制疫苗接种最核心的规则就是“应种月龄”。以国家免疫规划疫苗为例乙肝疫苗第1剂出生24小时内接种第2剂在1月龄第3剂在6月龄麻腮风疫苗第一剂在8月龄。这类规则如果写死在Java代码里后面医院扩展非免疫规划疫苗会很痛苦。我的做法是设计一张“疫苗接种程序表”字段包括疫苗名称、剂次、适用月龄下限月龄、上限月龄、间隔天数。每次给儿童生成应种计划时程序遍历当前儿童已完成的剂次记录结合出生日期和疫苗程序表计算出“当前日期是否落在某个剂次的应种窗口期内”是则给这条疫苗记录加一个“待接种”标记。public ListVaccinePlanVO buildPlan(Child child) { ListVaccinePlanVO result new ArrayList(); ListVaccineProgram programs vaccineProgramService.list(); for (VaccineProgram program : programs) { // 判断该儿童是否已接种过当前剂次 boolean done hasInoculated(child.getId(), program.getVaccineId(), program.getDose()); if (done) { continue; } LocalDate birthDate child.getBirthDate(); LocalDate latestDate birthDate.plusMonths(program.getApplicableMonthEnd()); if (!latestDate.isBefore(LocalDate.now())) { VaccinePlanVO vo new VaccinePlanVO(); vo.setVaccineName(program.getVaccineName()); vo.setDose(program.getDose()); vo.setSuggestDate(birthDate.plusMonths(program.getApplicableMonthStart())); result.add(vo); } } return result; }预约时段控制是并发防护的重点。一个门诊上午可能只放60个号每个号源时段对应一条slot记录包含总容量和已预约数。预约时不能先查再更新而是直接用一条带条件的updateUPDATE appointment_slot SET booked booked 1 WHERE id #{slotId} AND booked capacity如果这条update影响行数为0说明该时段已经约满直接提示家长换一个时间段。这比代码里先select再update要安全得多天然避免了并发超约。还要注意一个细节预约成功时要对疫苗批次做“锁定库存”操作但不直接扣减实际库存。预约锁定的数量在“取消预约”或“爽约”后要归还只有真正完成接种时才从实际库存里扣减。这样能避免家长预约了但没来接种导致的库存虚低。2.3 疫苗批次效期与库存防超卖疫苗批次是这类系统里最容易出问题的表。疫苗入库时管理员录入批号、生产日期、有效期系统要自动计算并标记状态。我在设计时留了几个状态字段正常、预警有效期前30天、过期有效期当天0点后、停用。效期判断不能只靠人工看我写了一个定时任务每天凌晨扫描一次所有批次Scheduled(cron 0 0 1 * * ?) public void checkBatchExpire() { ListVaccineBatch list vaccineBatchService.list(); for (VaccineBatch batch : list) { if (batch.getExpireDate().isBefore(LocalDate.now())) { batch.setStatus(expired); } else if (batch.getExpireDate().minusDays(30).isBefore(LocalDate.now())) { batch.setStatus(warning); } vaccineBatchService.updateById(batch); } }批量更新效率不高但在这种数据量不大的场景下完全够用。如果以后要支撑多家门诊改成一条update语句即可。库存扣减之所以要防超卖是因为同一款疫苗可能多个批次并存家长预约和门诊接种同时发生。扣减实际库存时我用了乐观锁机制boolean success vaccineBatchMapper.deductStock( batchId, num, expectVersion); if (!success) { throw new BusinessException(库存不足或数据已变更请刷新后重试); }UPDATE vaccine_batch SET stock stock - #{num}, version version 1 WHERE id #{id} AND stock #{num} AND version #{version}加上version字段作为乐观锁是对付并发最稳妥的办法。这里不建议用synchronized或者分布式锁因为一旦门诊并发量上来锁竞争会拖慢整个接种流程数据库乐观锁在绝大多数场景下已经足够。2.4 接种记录写入与异常回滚接种环节是整个系统里事务性最强的地方。门诊医生提交接种信息时一次操作涉及插入接种记录、更新儿童档案、扣减疫苗库存、更新预约状态。任何一步失败整个提交都应该回滚否则会出现“记录写了但库存没扣”这种事。核心代码如下Transactional(rollbackFor Exception.class) public void completeInoculation(InoculationDTO dto) { // 1. 插入接种记录 InoculationRecord record new InoculationRecord(); BeanUtils.copyProperties(dto, record); inoculationRecordMapper.insert(record); // 2. 更新预约状态为已完成 appointmentMapper.updateStatus(dto.getAppointmentId(), completed); // 3. 扣减库存乐观锁 vaccineBatchMapper.deductStock(dto.getBatchId(), 1, dto.getVersion()); // 4. 更新儿童档案的下次接种提醒可选 childMapper.updateNextVaccineDate(dto.getChildId()); }这里要特别强调“三查七对”在系统里的落地提交接种记录前前端和后端都要校验儿童姓名、疫苗名称、批号、有效期、剂量、接种部位、接种途径这七项。我在后端专门写了一个checkInoculationRule方法只要有一项不匹配直接抛业务异常不允许提交。不良反应上报不要做成强制弹窗否则医生嫌麻烦会漏报。我的做法是在接种提交成功后页面右下角出现一个“是否需要登记不良反应”的轻提示选择“是”才展开表单选择“否”就关闭。同时系统会把这个儿童标记为“疑似异常”后续同类疫苗预约时给出提醒。3. Vue3前端实战要点3.1 工程初始化与通用请求封装前端工程我直接用Vite创建命令很简单npm create vuelatest创建过程中可以选择TypeScript、Vue Router、Pinia这些选项。我建议打开TypeScript虽然刚开始写起来会麻烦一点但后期维护类型提示能省很多事。随后安装依赖npm install element-plus axios pinia vue-router dayjs echarts项目里最值得封装的是axios请求层。所有后端接口返回的都是Result结构前端统一在响应拦截器里处理数据解包和错误码。比如code为200时直接返回datacode为401时清理登录状态并跳转登录页其他code时ElMessage提示错误信息。// src/utils/request.js const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, 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(response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) })环境变量这块要单独提一下。开发环境后端地址通常是localhost:8080生产环境要换成真实域名。我在项目根目录建了.env.development和.env.production两个文件# .env.development VITE_API_BASE_URL/api VITE_PROXY_TARGEThttp://localhost:8080开发时通过Vite代理转发生产时由Nginx转发前端代码里不写死后端地址部署起来灵活很多。3.2 动态路由与按钮级权限控制后台管理系统的菜单通常是后端返回的前端根据用户角色动态生成。我的做法是登录成功后后端返回该用户的菜单列表和按钮权限编码前端把这些信息存Pinia里然后动态注册路由。// 动态路由注册核心逻辑 const modules import.meta.glob(../views/**/*.vue) function buildRoutes(menuList) { return menuList.map(menu ({ path: menu.path, component: modules[../views/${menu.component}.vue], children: menu.children ? buildRoutes(menu.children) : [] })) }按钮级权限用自定义指令v-permission实现比如“接种登记”页面里“提交接种”按钮只有角色编码为doctor的用户才可见app.directive(permission, { mounted(el, binding) { const required binding.value const hasPermission usePermissionStore().hasPerm(required) if (!hasPermission) { el.parentNode el.parentNode.removeChild(el) } } })el-button v-permissioninoculation:submit typeprimary提交接种/el-button这一层做好之后前端隐藏按钮只是体验层面的控制真正的权限校验肯定还是后端接口必须做的。前端隐藏只是为了不让普通用户看到入口不能指望它拦请求。3.3 预约日历与日期禁用实现预约页面是整个系统给家长用的核心页面。这里最容易被问到的功能是“怎么禁用某些日期”比如只能预约未来7天内的号源今天之前的日期不能选门诊休息日不能选。我用的Element Plus日期选择器通过disabledDate回调控制el-date-picker v-modelappointmentDate typedate :disabled-datedisabledDate placeholder选择预约日期 /function disabledDate(date) { const now new Date() const maxDate new Date(now.getTime() 7 * 24 * 60 * 60 * 1000) // 禁用今天之前的日期以及超过7天后的日期 return date.getTime() now.setHours(0, 0, 0, 0) || date.getTime() maxDate }如果业务要求“只能预约某个月份比如儿童满8月龄的那个月”那么可以在disabledDate里通过dayjs计算月份边界。这里有个小坑Element Plus的disabledDate拿到的date是0点时刻直接用getMonth()判断当月是可以的但要注意跨年和月末边界建议用dayjs统一比较import dayjs from dayjs function disabledDate(date) { const d dayjs(date) const targetMonth dayjs(2025-05-01) return d.isBefore(dayjs().startOf(day)) || d.month() ! targetMonth.month() }这类“日期禁用”逻辑看起来简单实际很容易漏掉时区问题。浏览器本地时间和服务器时间不一致时日期偏移会导致某个日期能选或者不能选。我这里统一约定所有预约日期的计算以后端返回的“可预约日期列表”为准前端只用disabledDate做第一层拦截真正能不能约以后端接口校验为准。接种登记页面还有个实用功能就是儿童档案的“快速查询”。门诊医生一天要接种一两百个儿童不可能逐个翻列表。我做了扫码枪输入支持儿童编号或证件号刷进去回车直接触发查询并进入登记页。这个功能对使用体验提升非常明显而且前端实现很简单就是input监听enter事件。3.4 Excel批量导入与报表看板儿童建档如果一个个手工录入医院会疯掉。我这边做了Excel批量导入前端用的是el-upload组件加xlsx库解析也可以直接把文件传到后端用EasyExcel解析。考虑到浏览器端解析Excel文件类型有限我个人更推荐后端解析方案前端上传文件后端读取并做逐行校验返回“成功多少条、失败多少条、失败原因在第几行”。el-upload action/api/child/import accept.xlsx,.xls :on-successhandleImportSuccess :show-file-listfalse el-button typeprimary批量导入儿童档案/el-button /el-upload后端用EasyExcel监听器逐行读取证件号重复、出生日期格式错误、监护人手机号位数不对这些都在导入时标记出来。报表看板我用ECharts做后端提供统计接口。比如统计近12个月各疫苗剂次接种量前端用柱状图展示儿童月龄分布用饼图门诊每日接种量趋势用折线图。报表部分开发量不大但要做得好后端SQL是关键。比如统计“本月一类疫苗接种完成率”就得左连接计划表和记录表按疫苗名称分组计算。4. 联调、部署与常见问题排查4.1 前后端联调与跨域配置前后端分离项目联调阶段最常遇到的就是跨域问题。开发环境我直接通过Vite代理解决vite.config.js里配置server: { port: 5173, host: 0.0.0.0, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这种方式的原理是浏览器请求的是前端同源的/api路径Vite开发服务器在中间做了转发从根源上避开了跨域。生产环境更简单Nginx配置中把/api路径转发到后端服务即可location /api { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }有同学会问既然前端和服务端是同一个域名下的/api为什么部署Vue应用时刷新页面会404这是因为Vue Router用了history模式路径像/admin/child/list这种在Nginx里找不到对应文件需要加try_files配置location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }4.2 常见问题速查表整个项目从开发到部署我把遇到过的典型问题整理成了一张速查表问题现象原因分析解决方案IDEA创建SpringBoot项目超时默认使用官方start.spring.io网络不稳定创建时选择阿里云镜像https://start.aliyun.comSpringBoot版本太高导致启动报错SpringBoot3.x依赖JDK17且javax包名变更稳定项目统一用2.7.x JDK8启动时MySQL表不存在没有手动执行初始化脚本项目启动前执行db/init.sql如要自动建表可配置spring.sql.init.modealways需先创建数据库application.yml里的数据库密码裸奔配置文件跟着代码仓库走容易泄露用Jasypt或环境变量注入敏感信息Vue项目启动后Network不可用默认host是localhostvite.config.js里设置server.host: 0.0.0.0axios请求一直Pending后端接口没启动或代理没配对检查后端启动日志、vite proxy target是否正确上传Excel超过1MB就报错默认上传文件上限是1MB后端配置spring.servlet.multipart.max-file-size10MB同时调整Nginx client_max_body_size前端打包后路由空白history模式没有Nginx fallback补上try_files $uri $uri/ /index.html4.3 打包上线与运维建议后端打包mvn clean package -DskipTests java -jar target/vaccine-system-0.0.1-SNAPSHOT.jar --server.port8080前端打包npm run build产物在dist目录直接拷贝到Nginx的html目录下。如果前端和后端部署在同一台机器强烈建议让Nginx代理后端接口避免前端直接暴露8080端口给外网。数据库初始化是上线最容易出问题的一环。我把建表SQL和基础数据菜单、角色、疫苗程序表分成两个脚本schema.sql和data.sql。上生产前先执行schema后执行data。如果后面要升级表结构再单独写migration脚本不要直接在旧库里乱改字段。定时任务这块也要注意。效期检查这类任务用Spring的Scheduled没问题但如果是“每天固定时间放号”“批量发送预约提醒短信”这类任务建议抽离到独立的调度服务里或者用开源任务调度平台避免和业务服务强耦合。5. 项目落地中的体会与后续扩展方向这类医疗业务系统做完之后我最大的感触是技术难度不是重点业务边界和异常流程才是决定项目成败的地方。所谓“边界”就是你要清楚每个状态机在什么条件下可以流转到哪里。比如预约单有“待接种、已完成、已取消、爽约”四种状态如果“已取消”还能去接种台完成接种那整个追溯链就断了。开发前应该把这类状态流转画成文字规则而不是边写边定。交付时医院那边让我做了个复盘有几点真实经验分享第一免疫程序模板一定要可配置。不同地区、不同级别门诊对同一款疫苗的剂次安排可能不一样把应种月龄和间隔天数写在代码里的做法三个月后必然返工。第二疫苗批号建议增加条码或者二维码扫码支持。虽然手工输入也能用但录入错误会导致批号追溯出问题扫码是最稳妥的方式。第三预约短信提醒不能省。儿童疫苗漏种率是门诊考核指标之一系统里我预留了发送通道对接位支持接入短信服务商。如果你做的是毕业设计这块用日志打印模拟也行但表结构要设计好。后续扩展我最建议的方向是微信小程序预约。现在家长对APP和网页的接受度其实一般公众号/小程序才是真正的入口。小程序端复用现有的预约接口只是把登录体系换成微信授权整体改动量并不大。另一个方向是对接医院HIS系统让儿童档案直接同步避免重复建档。如果门诊需要处理接种异常审批流程也可以引入Flowable这类工作流引擎比如“疑似预防接种异常反应”的专家诊断流程就可以用工作流跑起来。这套系统从零开发到上线前后大约花了六周时间后端三个模块、前端十个页面加上测试和修复节奏比较紧凑。如果让我重新做一遍我会在开发前花更多时间跟接种医生聊流程细节而不是对着需求文档闷头写代码。医疗系统的业务细节永远比你想的要多。