Spring Boot微信小程序新生儿疫苗预约系统设计与实现

发布时间:2026/10/9 9:18:39
Spring Boot微信小程序新生儿疫苗预约系统设计与实现
作为一名在社区医疗信息化领域折腾过好几个项目的开发者我太清楚新生儿疫苗预约这件事的痛点了。早年间社区接种点还在用现场排队人工登记的老模式家长抱着满月婴儿在走廊里挤成一团工作人员一边安抚哭闹的孩子一边手写登记表高峰期连下脚的地方都没有。后来陆续上过一些挂号平台但疫苗预约和普通门诊挂号完全是两码事——疫苗有批次、有时效、有库存、有间隔周期普通挂号系统根本扛不住这种复杂规则。所以当我拿到springboot社区新生儿疫苗预约小程序这个项目时第一反应是这确实是刚需。整套东西拆开来看后端用Spring Boot做核心服务前端跑在微信小程序里家长不用下载App扫一扫就能给新生儿预约接种。源码工程编号26885带完整的数据库脚本和前后端代码直接改配置就能跑起来。这篇文章我打算把整个项目的设计思路、核心表结构、预约流程的并发处理、以及我实际部署过程中踩过的那些坑从头到尾捋一遍。无论是想拿这个项目练手的同学还是社区医疗信息化的从业者应该都能从中找到点有用的东西。1. 社区新生儿疫苗预约的真实业务场景需求远不止挂号那么简单1.1 社区接种点的日常困境与预约系统的本质在做这个项目之前我专门去几个社区卫生服务中心蹲过点发现疫苗预约和普通门诊预约有个本质区别普通门诊是人来了就能看而疫苗预约是人来了还不一定有苗打。新生儿疫苗牵扯到国家免疫规划卡介苗、乙肝疫苗、脊灰疫苗、百白破疫苗每种疫苗的接种月龄、剂次间隔、库存批次都不一样。家长只知道该打疫苗了但具体该打哪种、有没有货、能不能打这些信息全在接种点工作人员脑子里。这个系统要解决的核心痛点有三个信息不对称家长不知道哪天有苗、哪个时段人少只能盲猜后跑到现场碰运气。规则复杂同一月龄可能同时接种两种疫苗不同疫苗之间有最短间隔天数要求预约系统必须能校验这些规则。高峰拥堵接种点通常上午扎堆下午空转需要把预约时段切细引导家长分流。1.2 需求梳理从家长端到接种医生端的完整闭环整个预约流程不是简单搞个选时间-提交就完了。我梳理需求时拆成了三个角色家长端微信小程序新生儿建档、绑定社区接种点、查看疫苗库存和可预约时段、提交预约申请、收到预约成功/提醒通知、查看接种记录。接种医生端管理后台维护疫苗批次和库存、设置每日放号数量和时段、核销预约码、记录实际接种信息、处理爽约和改期。系统端Spring Boot后端统一处理预约规则校验、库存扣减、时段锁定、状态流转还要给家长推送订阅消息。这一拆就会发现预约只是整个链条的入口真正的核心其实是库存管理和接种记录。库存管理决定家长能不能约上接种记录决定下一次该约什么。我把这两个模块放在数据库设计的最中心位置后面会详细说表结构。2. 技术选型推演为什么是Spring Boot配微信小程序2.1 后端框架选型Spring Boot相比其他方案的取舍先说我最初的候选方案Spring Boot、Node.js的Express、Python的FastAPI、微信云开发。最终选了Spring Boot主要基于三点考虑。第一生态成熟度。社区医疗信息化项目往往要对接后续的HIS系统医院信息系统、妇幼保健平台这些传统医疗IT栈基本都是Java系。Spring Boot作为Java生态里最主流的微服务框架后续做接口对接、二次开发、团队扩展都最顺畅。项目交付后如果社区卫生院自己的技术团队要维护Java人才也最好招。第二事务与并发控制。疫苗预约涉及库存扣减、预约记录创建、时段锁定三个操作必须放在同一个数据库事务里。Spring的Transactional声明式事务管理加上Lock悲观锁、Redis分布式锁处理这种场景非常顺手。相比之下Node.js和FastAPI虽然也能做但团队要写更多底层代码去保证一致性。第三和微信小程序生态的适配性。微信小程序前端通过HTTPS调用后端RESTful API后端只需要提供JSON格式的接口。Spring Boot的RestController天然就是干这个的配合springdoc-openapi可以自动生成接口文档给前端同学对接时省了大量沟通成本。2.2 前后端通信架构与接口约定整个系统采用前后端完全分离的架构微信小程序是纯前端运行在微信客户端里Spring Boot后端部署在云服务器上对外暴露HTTPS接口数据落在MySQL里热点数据比如每日剩余号源放在Redis里。接口层面我统一遵循RESTful风格按资源划分路由资源方法路径用途疫苗信息GET/api/vaccines获取疫苗列表及库存状态预约时段GET/api/schedules/{date}按日期查可约时段预约申请POST/api/appointments提交预约预约状态PUT/api/appointments/{id}/cancel取消预约接种记录POST/api/records医生端录入接种记录用户信息GET/api/users/{openid}获取家长及宝宝信息之所以把接口拆这么细是为了让小程序页面可以按需加载。比如首页只需要疫苗列表和库存状态就不需要把预约历史也一股脑返回减少流量消耗和首屏渲染时间。2.3 为什么不用微信云开发现在很多小程序教程推荐直接用微信云开发免服务器免域名数据库和云函数都现成。我明确不建议在这个项目里用云开发原因很现实数据安全与系统集成。云开发的数据存储在微信私有云环境里虽然对小项目够用但社区医疗数据后续要对接上级卫健委平台、妇幼保健网络这些系统基本只认传统关系型数据库和标准接口。用云开发数据提取和接口对接会非常别扭。另外项目既然用了Spring Boot说明后续大概率要扩展管理系统、报表分析等功能这些传统后端能力在云开发里写起来会非常痛苦。3. 数据库设计与预约核心流程的建模3.1 核心表结构一览数据库设计是整个项目最关键的部分。我在设计时遵循一个原则表结构要能完整还原疫苗预约的业务语义而不是简单堆字段。建了下面这几张核心表user表家长用户表字段类型说明idbigint主键openidvarchar(64)微信用户唯一标识nicknamevarchar(50)昵称phonevarchar(20)手机号baby_namevarchar(20)宝宝姓名baby_birth_datedate宝宝出生日期community_idbigint所属社区接种点IDcreate_timedatetime创建时间vaccine表疫苗信息表字段类型说明idbigint主键namevarchar(50)疫苗名称如乙肝疫苗categoryvarchar(30)类别一类/二类target_month_minint适龄最小月龄target_month_maxint适龄最大月龄interval_daysint上一剂次后最少间隔天数stockint当前库存descriptiontext疫苗说明appointment表预约记录表字段类型说明idbigint主键appointment_novarchar(32)预约单号user_idbigint用户IDvaccine_idbigint疫苗IDschedule_slot_idbigint预约时段IDappointment_datedate预约日期statustinyint状态0待接种/1已完成/2已取消/3爽约cancel_reasonvarchar(100)取消原因create_timedatetime创建时间schedule_slot表时间段表字段类型说明idbigint主键vaccine_idbigint疫苗IDslot_datedate日期start_timetime开始时间end_timetime结束时间total_quotaint总配额booked_countint已约数量versionint乐观锁版本号其实还有一张vaccination_record表接种记录表记录实际接种情况包括接种部位、批号、接种医生、下一剂预约建议日期这在疫苗全程追溯里特别重要。3.2 预约时段与库存的设计思路这里我重点说下schedule_slot表的设计。疫苗接种预约不是全天候随时都能约的接种点有固定工作时间比如上午8:00-11:30下午14:00-17:00。我把工作时间切成15分钟一个的时段每个时段对应一个schedule_slot记录疫苗关联多个时段。这样设计有几个好处精确控制人流15分钟放3个号一天下来接待量就是可控的。按疫苗分时段遇到特定疫苗比如卡介苗只在固定日期接种可以单独控制某个日期的时段开放状态。库存和号源分离疫苗库存是总库存号源配额是每日可接待量。疫苗有货但当天号已约满显示的是约满而不是无货业务语义更准确。3.3 预约状态机的流转设计预约状态不能只用一个字段存几个数字状态之间的流转规则如果不在代码里控制死后面会被各种异常操作搞乱套。我设计了这样的状态机待接种(0) → 已完成(1) 医生核销接种完成 待接种(0) → 已取消(2) 家长主动取消未过期 待接种(0) → 爽约(3) 超过预约时段且未取消 已完成(1) → 终态 已取消(2) → 终态 爽约(3) → 终态但可重新预约状态流转的代码逻辑都收口在Spring Boot的Service层小程序端只能通过特定接口触发状态变更不允许直接修改state字段。这样后续如果要加审核流程或异常处理也是在Service层加逻辑接口层保持稳定。4. 后端核心模块实现预约接口与并发防重的设计实践4.1 预约接口的完整实现链路预约接口是整个后端最核心的一个接口流程远不止插入一条记录那么简单。我贴一下核心实现思路的代码Override Transactional(rollbackFor Exception.class) public AppointmentResult createAppointment(AppointmentRequest request) { // 1. 校验用户是否已实名建档 User user userMapper.selectById(request.getUserId()); if (user null || !StrUtil.isNotBlank(user.getBabyName())) { return AppointmentResult.fail(请先完成宝宝档案信息填写); } // 2. 校验疫苗适龄规则 Vaccine vaccine vaccineMapper.selectById(request.getVaccineId()); int monthAge Period.between(user.getBabyBirthDate(), LocalDate.now()).getMonths(); if (monthAge vaccine.getTargetMonthMin() || monthAge vaccine.getTargetMonthMax()) { return AppointmentResult.fail(宝宝当前月龄不在该疫苗适龄范围内); } // 3. 查询可预约时段并加锁 ScheduleSlot slot scheduleSlotMapper.selectByVaccineIdAndDate( request.getVaccineId(), request.getAppointmentDate()); if (slot null || slot.getBookedCount() slot.getTotalQuota()) { return AppointmentResult.fail(该时段已约满); } // 4. 更新已约数量乐观锁 int updated scheduleSlotMapper.incrementBookedCount(slot.getId(), slot.getVersion()); if (updated 0) { return AppointmentResult.fail(时段刚刚被约满请选择其他时段); } // 5. 生成预约记录 Appointment appointment new Appointment(); appointment.setAppointmentNo(generateAppointmentNo()); // ... 设置其他字段 appointmentMapper.insert(appointment); return AppointmentResult.success(appointment); }这个接口里有几个必须注意的细节。第一步先校验用户档案避免没完成建档的裸用户直接预约最后核销时发现宝宝信息都没有闹出医疗差错。第二步的适龄规则校验monthAge的计算逻辑我专门封装了一个VaccineRuleValidator组件因为不同疫苗的月龄计算方式有细微差别——有的是自然月有的是按周数折算。4.2 库存扣减的三种方案对比与最终选择预约并发防重是这类系统的命门。一个热门疫苗放出50个号如果50个人同时提交搞不好会超卖。我实际对比过三种方案方案一数据库悲观锁SELECT FOR UPDATESELECT * FROM schedule_slot WHERE id ? AND vaccine_id ? FOR UPDATE这个方案最简单查到时段记录时直接锁行直到事务提交才释放。缺点是并发量大时数据库连接会堆积高并发场景下容易拖垮连接池。但对社区预约这种量级一天几百个预约是够用的。方案二乐观锁版本号控制就是我上面代码里用的方案。UPDATE ... WHERE version ?如果影响行数为0说明数据已被其他事务修改需要重试或者直接提示约满。优点是并发性能好缺点是冲突多时需要重试逻辑。方案三Redis分布式锁用SETNX对预约操作加锁锁的粒度可以精细到疫苗ID日期时段。这个方案最适合真正的高并发场景但引入了额外的中间件运维成本高。最后我的选择是乐观锁为主Redis分布式锁为辅。普通预约走乐观锁因为社区接种点的并发量远远到不了乐观锁需要频繁重试的程度但如果要做秒杀型的放号活动比如某疫苗只放10个号提前宣传某天上午10点准时放号那就在createAppointment方法入口加Redis锁双保险。这里给你一个操作用的代码参考public AppointmentResult createAppointmentWithLock(AppointmentRequest request) { String lockKey vaccine:appointment: request.getVaccineId() : request.getAppointmentDate() : request.getSlotTime(); boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (!locked) { return AppointmentResult.fail(当前预约人数过多请稍后重试); } try { return createAppointment(request); } finally { redisTemplate.delete(lockKey); } }注意锁的粒度一定要精细到具体时段如果只锁到疫苗级别一个疫苗的某个时段约满了其他时段也没法约这个体验就很差了。4.3 订阅消息通知让家长不用时刻盯着小程序预约成功之后不能就算完事了新生儿家长最怕的是忘了接种时间。微信小程序的订阅消息是这里最合适的通知方式。我在用户提交预约成功后后端调用微信接口发送订阅消息public void sendAppointmentSuccessNotice(Appointment appointment, User user) { MapString, Object data new HashMap(); data.put(thing1, new WxTemplateData(user.getBabyName() 的疫苗预约)); data.put(date2, appointment.getAppointmentDate() appointment.getStartTime()); data.put(thing3, appointment.getVaccineName()); wxMpService.getTemplateMsgService().sendTemplateMsg( WxMpTemplateMessage.builder() .toUser(user.getOpenid()) .templateId(订阅消息模板ID) .data(data) .build() ); }这里有个微信的坑必须提醒你订阅消息是一次性的。用户点了同意订阅只代表下一次消息能收到一次预约流程里我们得在合适的位置多次触发订阅。我的做法是预约提交后请求一次订阅接种前一天再通过小程序端的另一个按钮触发第二次订阅用两次订阅机会覆盖预约成功提醒和接种前提醒两个场景。这也是微信官方推荐的做法一次性订阅消息就是想明白让用户每次都有知情权。5. 小程序前端实现从授权登录到预约完成的路程5.1 微信登录与手机号授权的实现细节小程序端的登录流程我用的是微信官方推荐的wx.login获取code然后通过后端接口换成openid和session_key再签发自定义登录态。后端这层换code的接口我把它做成了标准的Spring Boot接口GetMapping(/api/auth/login) public Result login(RequestParam String code) { // 调用微信接口换取openid WxMaJscode2SessionResult session wxMaService.getUserService() .getSessionInfo(code); String openid session.getOpenid(); // 查库或创建用户 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } // 签发自定义token可用JWT或redis中保存session String token UUID.randomUUID().toString(); redisTemplate.opsForValue().set(user:token: token, openid, Duration.ofDays(7)); return Result.success(new LoginVO(token, user)); }手机号获取以前是wx.getPhoneNumber就能拿现在微信改版后手机号必须是企业认证的小程序、且通过getPhoneNumber按钮组件触发回调里会给一个加密的code后端拿到code再调接口解密。有个关键点要说清楚2023年之后新注册的小程序手机号快速验证组件要求必须用户手动点击不能再静默获取。所以我在家长绑定社区时专门设计了一个带open-typegetPhoneNumber的按钮让用户主动授权。5.2 首页疫苗信息展示与社区绑定首页我设计了三块内容顶部是宝宝信息卡片未建档时显示点击建档按钮中间是社区接种点切换器下面是可预约疫苗列表每条卡片显示疫苗名称、适龄月龄、库存状态有号/约满/无货。社区绑定这里有个业务细节一个家庭可能跨社区接种。比如很多家长房子在A社区但工作日住在B社区希望去B社区接种。所以我没有把社区绑定做成一个下拉框发生在注册时而是允许在首页随时切换。切换后的社区ID会被记录到user.community_id字段查询可约疫苗时按这个字段过滤。小程序端基于原生微信小程序写的不用uni-app主要是想保持这套代码的纯粹性。预览页面就是简单的WXMLview classvaccine-card wx:for{{vaccineList}} wx:keyid bindtaponVaccineTap># 1. 打包 mvn clean package -DskipTests # 2. 上传jar包到服务器 scp target/vaccine-app-1.0.0.jar root服务器IP:/opt/vaccine/ # 3. 创建数据库并执行初始化脚本 mysql -u root -p vaccine_app.sql # 4. 创建table_config配置如果用到Nacos或Apollo # 本项目使用application.yml直接修改连接串后打包 # 5. 启动服务 cd /opt/vaccine nohup java -Xms512m -Xmx1024m -jar vaccine-app-1.0.0.jar \ --spring.profiles.activeprod vaccine.log 21 # 6. 配置Nginx反向代理 # server块中配置 SSL证书、转发到8080端口6.2 微信小程序真机调试与审核注意事项小程序端和传统Web开发最大的区别是必须走微信的审核流程。医疗健康类小程序的审核比普通电商类严格得多我踩过的教训包括类目资质涉及疫苗预约微信要求提供医疗机构执业许可证或与接种点的合作关系证明。没有这个资质小程序根本过不了审核。如果只是学习演示可以在设置-基本设置-服务类目里选工具-预约类目但功能上必须避免出现医疗诊断等敏感词。隐私协议涉及采集手机号、宝宝出生日期必须在wx.config里配置隐私协议在《用户隐私保护指引》中声明采集这些信息的目的。订阅消息模板选择模板里的关键词必须和实际发送内容一致不能把宝宝姓名塞到物品名称字段里否则审核被驳回。真机调试时的另一个常见坑是局域网调试。小程序开发者工具里可以勾不校验合法域名但真机上不行。真机预览时必须把后端接口域名配置为HTTPS的合法域名且域名必须完成ICP备案。6.3 生产环境下的安全加固项目上线前做安全加固时我重点处理了三个问题接口鉴权所有/api/**接口除了登录接口都加了Token校验拦截器。拦截器从请求头取token去Redis查openid查不到就返回401。这里不建议用JWT静态密钥做无状态鉴权因为你无法在服务端踢人用户信息变更后旧token还继续有效。越权防护这是医疗系统最容易出的安全问题。家长A调GET /api/appointments/{id}查看预约详情如果后端不做校验他传别人的预约ID就能看到别人的接种信息。我所有涉及用户数据的接口后台都会校验appointment.getUserId()是否等于当前登录用户的ID不匹配直接拒绝。数据脱敏用户手机号、身份证号如果有在接口返回时脱敏比如手机号只展示前3后4位。微信小程序端界面展示的宝宝出生日期也只显示年月不显示具体日降低隐私泄露风险。7. 开发过程中踩过的坑和排查链路记录7.1 微信登录code2session的坑请求成功但用户信息拿不到第一次联调登录接口时前端一直报登录失败后端日志显示code2session调用成功但返回的openid是空。排查了半天最后发现是前端传的code已经过期。wx.login生成的code有效期只有5分钟但小程序冷启动时会自动调一次wx.login而我的前端代码在业务需要的时候又调了一次并且用的是第一次那个旧code。排查链路记录一下供你参考前端打开页面onLoad里调wx.login拿到code1。用户点了登录按钮页面没有重新调wx.login直接把code1传给后端。后端拿code1去换session微信返回40029 invalid code但错误日志不够明显被封装成了通用异常。解决方案每次需要登录时都重新调wx.login不要缓存code。把wx.login的调用放在点击登录的处理器里而不是页面的onLoad里。7.2 并发环境下预约数量超卖的复现与修复在压测阶段我用JMeter模拟30个并发请求预约同一个时段配额只剩10个结果数据库中产生了12条预约记录出现了超卖。排查过程很有意思。最初我以为是乐观锁没生效检查后发现incrementBookedCount的SQL是UPDATE schedule_slot SET booked_count booked_count 1, version version 1 WHERE id #{id} AND version #{version}这里booked_count booked_count 1这个操作本身就是原子的配合version条件确实不会超卖。但问题出在方法顺序我是在Service层先查询了slot对象再基于查询到的version做更新。高并发时30个请求都查到了相同的version虽然只有一个能更新成功但剩下的请求走的是约满返回分支理论上不会超卖。后来深挖日志才发现超卖发生的路径是从前端绕过了预约接口。有个测试脚本直接调了两次预约接口第二次带着第一次的请求数据重放而我的接口没有做幂等性处理导致同一时刻多个请求绕过乐观锁。修复方式很简单在appointment表加唯一索引(user_id, vaccine_id, appointment_date, schedule_slot_id)插入时如果冲突就抛出异常再配合乐观锁双保险从根上杜绝了同一个用户同一时段重复预约超卖。这也是做预约类系统必须上的一道保险。7.3 小程序端时间格式与后端时区不一致导致日期错乱这是一个非常隐蔽的坑。家长预约9月10日的疫苗后端收到请求后存到库里却是9月9日前端显示更是错乱。排查下来的根因是JSON序列化时的时间格式没指定时区。后端用Jackson序列化LocalDate时默认使用了服务器时区。但前端传参数是用字符串传的比如2025-09-10 09:00后端解析时用了默认时区如果默认时区是UTC解析出来的LocalDateTime会被当成UTC时间存储查询时再按Asia/Shanghai显示就差了8个小时。解决方案是全局配置Jacksonspring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss同时在传输层约定前端传所有时间参数时一律传字符串不传时间戳后端统一用DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)接收。这样把时间转换逻辑集中在Spring MVC的Converter层避免各个接口各自为政。7.4 订阅消息审核被驳回的一次完整修复记录最让我崩溃的是订阅消息模板前前后后被微信审核驳回了三次。第一次是模板标题用了接种提醒但模板类目下没有这个关键词被驳回了。第二次改成了服务状态通知但发送内容里写了您的宝宝XXX的疫苗预约已成功微信认为宝宝姓名属于敏感个人信息不能在订阅消息里透出完整姓名。最终方案是调整文案模板标题用预约状态通知内容字段改为服务类型疫苗预约预约日期2025-09-10 09:00温馨提示请按时携带《预防接种证》前往接种这样所有字段都变成中性描述没有具体的人名、疾病名审核一次通过。经验就是医疗类小程序做订阅消息文案越中性越好不要让审核人员觉得你在采集或展示敏感医疗信息。8. 项目扩展方向与性能优化思考8.1 从单社区到多社区的横向扩展当前的代码结构是按单社区社区ID作为用户表的一个字段设计的但如果要面向整个区县推广每个接种点需要独立的管理后台、独立的医生账号、独立的库存和号源配置。这个扩展的核心改动在数据结构需要新增community表并把vaccine、schedule_slot、appointment表全部通过community_id关联。后端接口也要从按用户所在社区过滤改成用户主动选择社区后按社区查询目前首页已经做了社区切换器所以这块改起来不算大。8.2 疫苗缺货提醒与排队池预约系统最怕的就是大量家长反复刷新看有没有号。与其让用户死磕GET /api/schedules接口不如做一个缺货提醒功能家长在某个疫苗约满时点到货提醒后端把这个用户ID记入Redis的队列等该疫苗的库存更新时通常是新批次入库给所有排队用户发送订阅消息。这个功能能极大降低接口压力也能提升家长的使用体验。我测算过即便一个社区有500个家长订阅了同一个疫苗一次性推送也就500条订阅消息完全在微信的免费额度内。8.3 接种记录与生长发育档案联动现在系统里已经有了vaccination_record表逻辑上可以扩展出宝宝成长档案。每次接种记录关联身高体重数据就能生成一个时间轴风格的生长发育视图。这虽然不是疫苗预约的核心功能但能显著提升家长打开小程序的频次让社区疫苗接种从一次性预约工具转变成宝宝健康管理入口。这个扩展方向我会在下一版迭代里重点考虑。我在实际开发这个项目的过程中最大的体会是疫苗预约系统的复杂度不在功能多而在于每条业务规则背后都对应着真实的公共卫生安全底线。适龄校验、间隔天数、批次追溯、状态流转任何一个环节偷懒都可能造成实际接种事故。所以写代码时一定不要嫌校验逻辑麻烦那些看着繁琐的规则检查才是这个系统的价值所在。整体跑下来这套Spring Boot 微信小程序的架构无论从开发效率、部署运维还是后续扩展性上都非常适合社区医疗这类场景代码结构也相对干净值得你直接拿来参考或二次开发。