
社区新生儿疫苗预约这个小程序我用Spring Boot把后端撸了一遍源码放在文末对应的编号里了。这个项目说白了就是解决社区医院的疫苗预约难题——以前家长要么现场排队、要么电话登记社区工作人员每天要接无数个电话去核对宝宝月龄、疫苗库存、接种时间不仅效率低还容易漏登记。这个系统把家长端小程序和管理端后台打通家长在线建档、按疫苗种类和时间段预约社区端统一审核和核销一套流程下来双方都省心。这个项目最适合两类人一是社区医院、卫生服务中心的信息化负责人想找一个能落地、能改的预约系统参考二是Java后端学习者尤其想搞明白Spring Boot 微信小程序这种前后端分离项目到底怎么组织、怎么部署。我会把项目的整体设计、数据库表结构、核心接口实现、部署细节和踩过的坑全部拆开讲尽量让照着做的人少走弯路。1. 项目整体设计与需求拆解1.1 社区疫苗预约场景的核心痛点社区新生儿疫苗预约和普通门诊挂号有很大区别。普通门诊挂号只需要选科室、选医生、选时间但疫苗预约必须校验宝宝的月龄和疫苗种类是否匹配比如乙肝疫苗第一针必须在出生后24小时内接种第二针要求满1月龄第三针满6月龄这些规则错了接种机构是要担责的。另一个痛点是库存问题。社区医院的疫苗通常按月从区疾控中心领取冷链存储空间有限每个接种日的号源不能超过库存上限否则当天没疫苗可打家长白跑一趟。电话预约时代工作人员只能用Excel登记边接电话边查库存非常容易出错。还有爽约问题。传统电话预约的爽约率很高因为没有成本家长约了不来社区医院空出时段其他想打的宝宝又约不上。引入线上预约后可以通过限制同一宝宝同时只允许一个有效预约、逾期未核销自动释放号源等机制来约束。1.2 用户角色与核心业务流程这个项目设计了三个核心角色家长端用户通过小程序注册登录添加宝宝信息查看疫苗计划预约接种时间查询接种记录。社区接种医生通过管理后台核验预约信息确认接种登记疫苗批号记录接种结果。系统管理员维护疫苗目录、库存量、可预约时段、公告信息查看统计数据。核心流程是这样闭环的家长在小程序里选择疫苗种类系统自动校验宝宝月龄是否符合接种要求再展示未来7天可预约的时段和剩余号源。家长选定时段提交预约生成待审核状态。医生在后台看到待审核列表核对宝宝档案和疫苗库存后点击通过家长端收到预约成功的通知。接种当天医生扫描家长的预约二维码确认信息无误后执行接种系统自动扣减库存并生成接种记录。这里有几个容易被忽略的细节。第一个是预约审核环节很多初版设计会把预约直接置为成功但实际社区接种场景里医生需要人工确认宝宝的近况——比如近期有没有发烧、有没有接种禁忌症所以必须保留人工审核。第二个是号源释放如果审核不通过号源必须及时回滚不然库存账目会越积越不对。1.3 功能清单梳理我按照用户角色整理了一份功能清单做项目的时候可以直接抄这个表来拆分开发任务角色功能模块具体功能点家长端用户登录微信授权登录、手机号绑定家长端宝宝管理添加/编辑宝宝档案、查看接种计划家长端疫苗预约疫苗列表、月龄校验、时段选择、提交预约家长端预约管理查看预约状态、取消预约、预约二维码家长端接种记录历史接种记录、下次接种提醒医生端预约审核待审核列表、通过/驳回、号源回滚医生端接种核销扫码核销、疫苗批号登记、接种结果录入医生端库存管理疫苗入库、库存预警、批次管理管理员基础配置疫苗目录、可预约时段、公告发布管理员统计报表每日预约量、接种量、爽约率统计时间充足的话再加一个消息通知模块用微信订阅消息给家长推送审核结果和接种提醒这个功能很实用可以明显降低爽约率。2. 技术选型与架构设计2.1 为什么选Spring Boot做后端社区医院这类场景项目规模不大但后续可能要对接区级公共卫生平台所以技术选型不能太另类要选社区Java开发最容易维护的。Spring Boot是目前国内中小型系统最主流的选择没有之一。选Spring Boot有几个实际理由。第一是生态成熟微信小程序后端该用的东西都有现成的starter——微信登录解析、MyBatis Plus操作数据库、Redis缓存、定时任务全部开箱即用。第二是部署简单打包成一个Jar文件扔到服务器上就能跑社区医院的服务器配置通常不高Spring Boot的内嵌Tomcat省去了单独装容器的麻烦。第三是招人容易社区医院的信息化项目大概率由第三方维保找一个熟悉Spring Boot的工程师比找熟悉Go或者Python的人容易得多。2.2 小程序端技术方案选择小程序端我用了原生微信小程序没有用uni-app或者Taro。理由很简单这个项目不需要跨端只在微信生态内使用原生语法虽然啰嗦一点但调试最方便微信开发者工具对原生项目的支持也最到位。原生小程序的页面结构是WXML WXSS JS JSON四件套数据绑定和事件处理都比较直观。对于疫苗预约这种表单密集型应用原生的picker组件选择日期和时间非常顺手map组件展示社区接种点位置也够用。如果团队里有Vue经验丰富的人可以考虑uni-app一套代码可以同时发布小程序和H5管理端。但我的建议是不要为了省事引入额外的编译层小程序的原生组件在真机调试时最稳定遇到问题网上的解决方案也最多。2.3 核心依赖与中间件选型后端依赖清单如下Spring Boot 2.7.x版本注意不要选3.x原因我在后面避坑部分详细说MyBatis Plus 3.5.x单表CRUD不用写SQL分页查询用内置的Page插件MySQL 8.0存储业务数据Redis 2.6.x对应的starter存验证码、短时重复提交标记和缓存疫苗库存Hutool工具包处理日期、随机数、Bean拷贝等杂活微信小程序登录相关使用官方api的WxMaService这里重点说下为什么用Redis。疫苗预约对并发控制的要求比较高比如某个热门疫苗有50个号源几十个家长同时抢如果直接对MySQL做库存扣减很容易出现超卖。用Redis的Lua脚本做库存预扣再把订单信息异步写入MySQL可以确保库存不超。这个项目的并发量虽然不大但从一开始就做好库存隔离设计后期加号源时心里有底。3. 数据库设计与核心表结构3.1 核心表设计思路数据库我设计了7张核心表分别是家长用户表、宝宝表、疫苗表、预约表、接种记录表、系统用户表与公告表。其中预约表是全项目的核心我把字段结构列出来CREATE TABLE vaccine_appointment ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, appointment_no varchar(32) NOT NULL COMMENT 预约编号, baby_id bigint NOT NULL COMMENT 宝宝ID, user_id bigint NOT NULL COMMENT 家长用户ID, vaccine_id bigint NOT NULL COMMENT 疫苗ID, appointment_date date NOT NULL COMMENT 预约日期, time_slot varchar(10) NOT NULL COMMENT 时段: AM/PM, status tinyint NOT NULL DEFAULT 0 COMMENT 状态:0待审核,1已通过,2已接种,3已取消,4未核销, vaccine_batch_no varchar(64) DEFAULT NULL COMMENT 疫苗批号, doctor_id bigint DEFAULT NULL COMMENT 核销医生ID, cancel_reason varchar(255) DEFAULT NULL COMMENT 取消原因, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime NOT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_appointment_no (appointment_no), KEY idx_baby_id (baby_id), KEY idx_vaccine_id_date (vaccine_id, appointment_date) ) ENGINEInnoDB COMMENT疫苗接种预约表;预约编号用日期加随机数生成格式是20250120加6位随机字符确保视觉上短且唯一。唯一索引放在预约编号上业务操作都用这个编号做关联不直接用自增主键对外暴露避免别人遍历你的数据。3.2 预约状态机设计预约状态的变化是整个系统的核心逻辑我建议在代码里用一个状态机工具类来管理不要在Service层到处写状态判断否则后期改一个状态流转要翻遍整个项目。这个项目里的状态流转是这样的待审核可以变为已通过或已取消家长取消或者医生驳回都从待审核直接进入已取消同时回滚号源已通过可以变为已接种或未核销。到接种日当天24点还没有核销记录的定时任务自动把状态置为未核销释放号源已接种是终态不可再变已取消也是终态如果要重新预约家长走新建预约流程设计状态机时有一件事必须考虑清楚驳回和取消要不要释放库存。我的做法是只要预约状态从非终态变为已取消立即把对应的疫苗库存余额加回。这里要小心并发库存回补也要走Redis的Lua脚本防止重复回补导致库存虚高。3.3 数据一致性保障疫苗预约最怕数据不一致典型的场景是医生在后台点了通过但库存已经在超卖边缘实际没有号了。我的处理方案是用两阶段校验第一阶段是预约提交时校验。家长提交预约前端先通过接口查询可预约数大于0才允许进入填写页面提交时后端再次校验当前库存数大于0才创建待审核预约。第二阶段是医生审核时校验。医生点击通过后端先执行数据库层面的乐观锁更新update vaccine_stock set remaining remaining - 1 where vaccine_id ? and remaining 0更新行数为0说明库存已经被抢完直接驳回这条预约。MySQL的affected rows判断是保证不超卖最朴素也最可靠的方法比在应用层加同步锁更干净。配合Redis的预扣机制双保险实测并发下数据完全一致。4. 实操关键功能的实现4.1 微信小程序登录与用户绑定小程序登录的流程网上文章很多但写代码时有几个细节值得注意。我用的是wx.login()获取code发送到后端换取openid然后以openid作为用户唯一标识。首次登录自动注册非首次直接返回token。这里有一个很常见的坑小程序端拿到的code只能用一次5分钟内有效。如果后端处理超时或者网络重试就会拿到一个已失效的code微信会返回40029错误。解决方法是前端从wx.login()成功回调里立刻把code发到后端不要等用户填写完信息再一起提交。用户表字段我除了openid还加了nickname、avatar、phone。phone是后来做手机号快捷绑定用的社区场景里很多家长年纪偏大不喜欢每次登录都输手机号这个快捷授权能省不少事。4.2 疫苗库存与预约时段控制疫苗库存我设计了两层数据库表vaccine_stock存疫苗的总库存和剩余库存Redis用字符串类型存当天每个时段的可预约数key格式是vaccine:stock:{vaccineId}:{date}:{timeSlot}。预约时段逻辑是这样的每天分成上午和下午两个时段上午号源50个下午号源50个。每个疫苗在某个日期和时段的初始号源在系统管理员配置时写入Redis家长每次查看可预约列表直接读Redis返回剩余号源。这样设计的好处是查询不压数据库预约高峰期接口响应速度可以保持在100毫秒以内。库存预扣的Lua脚本我贴在下面这段脚本是库存防超卖的核心也是我在网上踩了无数坑以后总结出来的写法-- KEYS[1]: 库存key -- ARGV[1]: 扣减数量 -- ARGV[2]: 当前时间戳 local stock redis.call(GET, KEYS[1]) if not stock then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return 0 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 1Redis的DECRBY命令本身是原子性的但先查后减的两步操作有并发间隙所以必须用Lua脚本把查询和扣减放在一个原子操作里。社区项目并发不高这套方案完全够用不需要引入Redisson这种分布式锁框架。4.3 预约流程的后端实现预约接口的完整逻辑分成五步校验用户登录态从token中解析userId查询宝宝是否存在且属于当前用户校验疫苗是否存在以及宝宝月龄是否满足接种要求调用Redis Lua脚本预扣库存失败则返回号源已约满创建预约记录状态置为待审核发送审核通知月龄校验是疫苗预约项目里最容易写错的地方。疫苗的接种月龄是按自然月计算的比如某疫苗要求满2月龄才能接种指的是宝宝出生满60天自然日而不是两个自然月。不同月份的天数不同不能简单用当前日期减去出生日期的月份来算。我的实现是使用Hutool的DateUtil.between方法计算天数差值再除以30天换算成月龄。public boolean checkAge(Long babyId, Long vaccineId) { Baby baby babyMapper.selectById(babyId); Vaccine vaccine vaccineMapper.selectById(vaccineId); long daysBetween DateUtil.between(baby.getBirthDate(), new Date(), DateUnit.DAY); // 将天数换算为月龄不足整月按实际天数算 int monthAge (int) (daysBetween / 30); return monthAge vaccine.getMinAgeMonth(); }这段逻辑在预审和医生审核时都要调用保证双端校验一致。4.4 管理端核销与统计医生端的核销功能最省事的方式是把预约二维码做成带参数的canvas图片医生登录后台用扫码枪或者手机扫一扫解析出预约编号后端根据编号查出预约详情。核销时要做三步校验预约状态必须是已通过、预约日期必须是当天、预约的疫苗和当前疫苗接种记录要对应。统计报表我直接用MyBatis Plus的LambdaQueryWrapper做分组查询按日期、疫苗、时段三个维度统计预约量和接种量。MySQL的GROUP BY在数据量不大的情况下完全够用不需要额外引入定时同步到数据仓库的组件。不过要注意索引优化预约表必须建(vaccine_id, appointment_date)联合索引否则统计查询会全表扫描数据量到万级就会明显变慢。5. 源码部署与二次开发5.1 环境准备与部署步骤拿到源码之后部署环境需要准备JDK 1.8、Maven 3.6、MySQL 8.0、Redis 5.0以及微信公众平台注册的小程序账号。这里我重点说一下怎么把项目跑起来避免新手在环境阶段卡两天。后端环境配置在application.yml文件里核心配置项如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/vaccine_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword redis: host: localhost port: 6379 wx: ma: appid: 你的小程序AppID secret: 你的小程序AppSecret数据库初始化脚本在sql目录下直接执行schema.sql即可建表。需要注意的一点是MySQL的时区设置本地没问题不代表服务器没问题连接串里必须加上serverTimezoneAsia/Shanghai否则日期字段会差8个小时预约日期的判断会全乱。前端小程序用微信开发者工具导入miniprogram目录在config.js里把baseUrl改成你的后端地址本地调试用http://127.0.0.1:8080真机预览时改成服务器IP或域名。注意微信开发者工具必须勾选不校验合法域名否则本地访问IP地址会直接报错。5.2 源码结构与核心代码解读源码的结构分为两大部分backend目录是Spring Boot工程miniprogram目录是微信小程序工程。用Maven构建时在backend目录执行mvn clean package -DskipTests生成的Jar包在target目录下然后用java -jar vaccine-appointment.jar启动。这里给你一个源码阅读的路线图按这个顺序看效率最高先看entity目录下的实体类对应数据库7张表把字段和表结构对照起来看再看mapper目录MyBatis Plus的BaseMapper接口很简单但要看懂条件构造器的用法接着看service目录重点是AppointmentService和StockService预约流程和库存扣减都在这两个类里最后看controller目录分wx和admin两组对应小程序端和管理端接口。后端接口按功能分组全部以/api/wx和/api/admin开头小程序端调用/api/wx/appointment/submit提交预约管理端调用/api/admin/appointment/audit审核预约。5.3 二次开发建议源码要落地到真实场景有几个点建议优先改第一是疫苗目录要对接当地疾控中心的疫苗编码体系不同省份对疫苗名称和编码的定义不完全一样源码里用了统一的拼音编码实际使用时要改成当地统一的编码规范。第二是增加短信通知能力。微信订阅消息有一次性模板的限制很多家长授权后只能收到一次通知后续的接种提醒就断了。稳妥的做法是接入阿里云短信或者腾讯云短信在预约审核通过和接种日前一天各发一条短信。第三是排班模块。现在的代码只支持上午下午两个固定时段但有的社区医院中午也开放或者有夜间接种建议把时段表抽出来单独建一张配置表让管理员可以动态维护每天的几个接种时段这个改动对数据库和前端都有影响但后期灵活度会高很多。6. 常见问题与排查技巧实录6.1 高频问题排查我在开发这个项目的过程中记录了一些比较典型的问题整理成一张速查表遇到问题可以先对照排查。问题现象可能原因解决方案小程序请求后端接口报404baseUrl配错或后端没有启动检查config.js的baseUrl和项目端口微信登录返回40029错误code已过期或被多次使用确保wx.login后立即发送code日期字段差8小时MySQL连接串没有设置serverTimezone连接串加serverTimezoneAsia/Shanghai预约按钮点击没反应库存Redis key没有初始化管理员在后台配置时初始化库存key医生端看不到待审核预约状态不对或权限角色不对检查预约记录状态是否为0待审核疫苗库存不减少Lua脚本报错或Redis连接失败查看Redis日志和Lua脚本返回值家长收不到审核通知订阅消息模板ID配置错误到小程序后台检查模板ID和跳转页面6.2 排障中的实战心得这里分享几个比较有价值的排障心得第一Spring Boot版本一定要慎重。我最初用Spring Boot 3.0开发结果项目里的很多第三方库都没适配Jakarta命名空间特别是MyBatis Plus的旧版本直接抛ClassNotFoundException。后来把版本退到2.7.x所有问题瞬间消失。如果你拿到的源码用的是2.7.x不要轻易升级到3.x除非你把所有依赖都一起升级并测试过。第二小程序端的wx.request请求默认超时时间是60秒但它有个致命的坑如果你在onLoad里同时发起三四个请求某些安卓机型会随机丢请求。务必要封装请求队列或者合并接口把宝宝信息、疫苗列表、可预约时段在一次请求里全部返回。第三Redis的key要有统一的过期策略。疫苗库存的key必须设置过期时间比如晚上12点过期第二天管理员配置新库存时重新生成。如果不设置过期时间时间一长Redis里会堆满无人问津的历史key内存会缓慢增长。第四所有涉及金额或敏感信息的地方都要做参数校验。疫苗预约本身不涉及支付但家长信息、宝宝出生日期这些属于敏感数据接口层一定要用Validated做入参校验防止有人恶意提交不合法数据。数据库的字段长度也要和校验规则对应不然写库时会出现截断错误。6.3 性能与并发优化建议如果后续预约量涨到很大比如一个社区医院一天要处理上千个预约有几个优化点可以提前安排第一个是接口缓存。疫苗列表和可预约时段属于读多写少的接口可以加一层Redis缓存设置30秒过期30秒内所有用户看到的数据一致查询压力大幅下降。第二个是异步处理。预约审核通过后要发送订阅消息、更新统计、回补库存这些都可以丢到Spring的Async线程池里异步执行主流程只负责更新状态响应时间会明显缩短。第三个是MySQL连接池参数。Spring Boot默认的HikariCP最大连接数是10如果在高峰时段接口响应变慢可以适当调大到20到50同时检查数据库的最大连接数限制。说实话对于社区新生儿疫苗预约这个体量上面的优化做到前两步基本就足够了不必过度设计。过度设计会让项目变得难以维护社区场景最重要的是稳定性和易维护性这一点我深有体会。7. 写在最后的一点体会这个项目前后花了大概三周时间开发前两周做后端接口和数据库设计最后一周做小程序页面和联调测试。最耗时的部分不是写代码而是理清疫苗预约的各种业务规则——每种疫苗的最小月龄、不同剂次的间隔天数、节假日接种时间调整这些业务规则的边界如果没理清代码写再多都是白费。我自己的体会是做这类社区垂直场景的系统技术难度真的不高价值全在业务细节里。你把家长约不上号、医生登记麻烦、库存对不上账这些细节解决好了系统上线就能真正帮到人。技术选型用最主流的Spring Boot加小程序原生后续维护和扩展都方便社区医院的信息化预算有限这样务实的方案反而最合适。源码下载后第一次跑通建议先用测试数据走一遍完整流程家长注册、添加宝宝、预约疫苗、医生审核、核销接种、查看记录每一步都确认无问题后再改代码。另外提醒一下部署上线前务必把小程序后台的服务器域名配好并且把config.js里的请求地址改成HTTPS的正式域名不然微信审核那关就过不了。