做社区医疗类项目这些年,我拆过不少带预约功能的 Spring Boot 程序,但专门围绕“新生儿疫苗”这个细分场景做的项目其实不多,大部分开源案例都停留在挂号、体检或者宠物预约上,真正把新生儿档案、疫苗库存批次、预约并发控制、接种核销这几件事完整串起来的源码项目很少见。这个标题里带“源码26885”的社区新生儿疫苗预约小程序项目,正好是我觉得值得拿来仔细拆一遍的类型。
它的定位非常明确:一个社区接种门诊或者社区卫生服务中心,面向辖区内的新生儿家长,提供疫苗知识查看、宝宝档案登记、疫苗预约、预约记录管理等功能,后端用 Spring Boot 提供接口,前端是微信小程序。我之所以建议做这块的人认真看一遍这种项目,是因为它麻雀虽小但五脏俱全——既有常规 CRUD,又有库存扣减这种高并发场景里的经典逻辑,还有小程序登录、接口鉴权、定时任务等实际上线必须处理的东西。这篇文章我就从源码阅读者的角度,把这个项目从表结构到预约主流程,再到小程序联调避坑,完整过一遍。
1. 新生儿疫苗预约到底在解决什么现实问题
1.1 社区接种门诊的日常有多依赖“预约”这件事
新生儿疫苗接种和普通门诊最大的区别在于计划性强。一类疫苗什么时候打、二类疫苗间隔多久补,国家免疫规划程序写得清清楚楚,比如乙肝疫苗要在出生后 24 小时内接种第一剂,脊灰疫苗要满 2 月龄才能开始打。社区门诊每个月的接种日有限,疫苗有批次有数量,新生儿数量不大但家长焦虑度高,经常出现的情况是:接种日当天早上门口排长队,下午疫苗库存打完了家长白跑一趟。
传统做法是电话通知或者家长群接龙,但这种方式的弊端很明显。负责计免的护士要在接种日前一天挨个打电话提醒,高峰期一天打几十个电话;家长在群里接龙经常覆盖掉其他人的消息,漏掉预约信息的情况时有发生。小程序预约出现之后,家长自己选择时间段、看到剩余号源、到点直接来接种,护士只需要在后台核销名单就行。这个项目解决的正是“社区卫生服务中心需要一套低成本、低维护成本、面向家长自助操作的预约工具”这个需求。
1.2 为什么选 Spring Boot 而非其他后端框架
社区级别的预约系统,并发量通常不会太高,一个社区卫生服务中心辖区内的新生儿,一年可能也就几百到一千多个。这种场景最忌讳的就是过度设计,不需要微服务,不需要消息队列,不需要分布式事务,一个单体 Spring Boot 应用就够了。
Spring Boot 在这个场景下的优势很清楚:内置 Tomcat,改了代码直接 java -jar 就能跑;配合 MyBatis-Plus 做单表 CRUD 几乎不用写 SQL;Spring MVC 提供 RESTful 接口给小程序调用;@Scheduled 注解能直接处理预约过期的批量释放。如果换成 Node.js 或者 Flask,也不是不能做,但在社区医疗这类偏传统的技术团队里,Java 系维护成本更低,招人更容易,部署在 Windows 或者 Linux 服务器上都方便。
1.3 源码包里能学到哪些东西
我建议拿到源码后按三条线去读。第一条线是业务主线,从 baby_info、vaccine_catalog、vaccine_stock、appointment_order 四张表入手,理解预约业务的数据流转;第二条线是接口链路,看 Controller 层如何接收小程序参数,Service 层怎么做校验和库存扣减,Mapper 层如何写条件更新;第三条线是工程化细节,比如拦截器里做 token 鉴权、全局异常处理器统一返回结构、配置类处理跨域和日期格式。
源码编号 26885 我猜测是项目归档或者资料库的编号,不用太纠结这个数字本身,解压之后重点看 src/main/java 目录下按 controller/service/mapper/entity 分包的结构即可。整体代码量不大,读起来不会有压力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术方案选型和工程结构导读
2.1 这套源码的技术栈组合
我整理了一下这个项目里能看到的技术组件,实际目录里可能因为版本差异略有不同,但大方向一致:
| 层级 | 技术选型 | 作用 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 提供 REST API,内置 Tomcat |
| ORM | MyBatis-Plus 3.5.x | 单表 CRUD,分页查询 |
| 数据库 | MySQL 8.x | 业务数据存储 |
| 鉴权方案 | JWT + 拦截器 | 小程序登录后的身份识别 |
| 工具库 | Hutool、Lombok | 生成编号、日期处理、简化实体 |
| 定时任务 | Spring @Scheduled | 处理过期预约释放库存 |
| 前端 | 微信原生小程序 | 家长端预约操作界面 |
| 可选组件 | Redis | 缓存疫苗目录、验证码频率控制 |
这里要提醒一句,如果你看到源码里没有 Redis,不要太意外,社区级别确实可以不用缓存,疫苗目录这种数据量很小的表直接查 MySQL 完全没压力。加上 Redis 反而增加了部署复杂度。这个项目的取舍思路值得学习:能用数据库解决的就不引入中间件。
2.2 Spring Boot 版本与运行环境匹配问题
源码里如果写的是 Spring Boot 2.7.x,对应 JDK 8 到 JDK 11 都可以跑;如果写的是 Spring Boot 3.x,那必须用 JDK 17 以上。很多人一上来运行报错,不是代码问题,而是 JDK 版本不对。
我建议拿到源码后先看三个地方:pom.xml 里的 <parent> 版本号、本机 java -version 结果、application.yml 里的数据库连接配置。看到 Spring Boot 2.7 的源码就不要用 JDK 17 去硬跑,否则会出现 java.lang.IllegalAccessError 这类诡异报错。更推荐的做法是装一个 JDK 8,然后在 IDE 里把 Project Structure 的 SDK 切到 1.8,编译器级别也切到 1.8。
2.3 后端工程结构源码导读
源码的包结构一般长这样,虽然命名可能略有不同,但逻辑是通用的:
code复制com.community.vaccine
├── controller
│ ├── WechatController.java # 小程序登录
│ ├── BabyController.java # 新生儿档案管理
│ ├── VaccineController.java # 疫苗目录与批次查询
│ └── AppointmentController.java # 预约单创建/取消/查询
├── service
│ ├── BabyService.java
│ ├── VaccineStockService.java
│ └── AppointmentService.java
├── mapper
│ ├── BabyInfoMapper.java
│ └── AppointmentOrderMapper.java
├── entity
│ ├── BabyInfo.java
│ └── AppointmentOrder.java
├── config
│ ├── WebMvcConfig.java # 拦截器注册
│ └── MybatisPlusConfig.java # 分页插件
├── interceptor
│ └── AuthInterceptor.java # token 鉴权
├── common
│ ├── Result.java # 统一返回结构
│ └── GlobalExceptionHandler.java # 全局异常
└── VaccinationApplication.java # 启动类
读源码的时候最常见的错误是上来就找 Controller 里某个接口,然后顺着调,结果越看越乱。正确姿势是先读实体类,再读 Mapper,接着读 Service,最后看 Controller,把数据模型装进脑子里之后再看接口就非常快了。
3. 核心数据表设计:照着建库就能跑通业务
3.1 新生儿档案表与疫苗目录表
预约业务的基础是“谁去打针”和“打什么针”。新生儿档案表 baby_info 的核心字段如下:
sql复制CREATE TABLE `baby_info` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`parent_openid` VARCHAR(64) NOT NULL COMMENT '家长微信openid',
`parent_name` VARCHAR(32) NOT NULL COMMENT '家长姓名',
`parent_phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
`baby_name` VARCHAR(32) NOT NULL COMMENT '宝宝姓名',
`baby_gender` TINYINT NOT NULL COMMENT '1男 2女',
`birth_date` DATE NOT NULL COMMENT '出生日期',
`vaccination_code` VARCHAR(64) DEFAULT NULL COMMENT '预防接种证编号',
`relation` VARCHAR(16) DEFAULT NULL COMMENT '与宝宝关系:父亲/母亲/其他',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_openid` (`parent_openid`)
) ENGINE=InnoDB COMMENT='新生儿档案表';
注意这里有几个容易被忽略的细节。parent_openid 加索引很重要,因为小程序端查询“我的宝宝列表”时最常用的过滤条件就是这个字段。deleted 逻辑删除字段是 MyBatis-Plus 的标配操作,配合 @TableLogic 注解使用,查询时会自动追加 deleted = 0 条件。出生日期用 DATE 类型而非 DATETIME,因为只需要精确到天,用来算月龄。
疫苗目录表 vaccine_catalog 对应的字段要能支撑月龄匹配和批次排班:
sql复制CREATE TABLE `vaccine_catalog` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`vaccine_code` VARCHAR(32) NOT NULL COMMENT '疫苗编码',
`vaccine_name` VARCHAR(64) NOT NULL COMMENT '疫苗名称',
`disease_target` VARCHAR(128) DEFAULT NULL COMMENT '预防疾病',
`is_free` TINYINT NOT NULL DEFAULT 0 COMMENT '1一类免费 2二类自费',
`min_age_month` INT NOT NULL COMMENT '最小接种月龄',
`max_age_month` INT DEFAULT NULL COMMENT '最大接种月龄',
`total_dose` INT NOT NULL DEFAULT 1 COMMENT '接种剂次',
`interval_days` INT DEFAULT NULL COMMENT '与上一剂间隔天数',
`remark` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_vaccine_code` (`vaccine_code`)
) ENGINE=InnoDB COMMENT='疫苗目录表';
3.2 疫苗批次与库存表:把排班变成可扣减的库存
这一步是整个系统设计的精华。很多初学的人会把预约做成“选日期 + 选疫苗”的组合,然后用程序逻辑去判断剩余名额,这样很容易出 bug。这个项目的思路是把“某一天某个时段可以打某一种疫苗”做成一条库存批次记录,每个批次有总数量和已约数量,预约时直接对该批次做库存扣减。
vaccine_stock 表设计参考如下:
sql复制CREATE TABLE `vaccine_stock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`catalog_id` BIGINT NOT NULL COMMENT '疫苗目录id',
`batch_name` VARCHAR(32) NOT NULL COMMENT '批次名称,如20250614上午',
`stock_date` DATE NOT NULL COMMENT '接种日期',
`time_slot` VARCHAR(32) NOT NULL COMMENT '时段,如08:30-10:00',
`total_count` INT NOT NULL COMMENT '该批次总号源',
`used_count` INT NOT NULL DEFAULT 0 COMMENT '已预约数量',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 2停用',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_catalog_date_slot` (`catalog_id`, `stock_date`, `time_slot`),
KEY `idx_stock_date` (`stock_date`)
) ENGINE=InnoDB COMMENT='疫苗批次库存表';
设计这个表时最需要想明白的一个概念:used_count 不是冗余字段,而是专门留给并发扣减用的。当两个家长同时在 10:00 整点预约同一批次最后两个号源时,如果先查剩余号再 insert 预约单,就会产生超卖;只有用一条带条件的 update 语句原子性地把 used_count 加一,同时判断 used_count 小于 total_count,才能保证不超卖。
3.3 预约订单表:业务状态闭环
预约单是整个流程的核心实体:
sql复制CREATE TABLE `appointment_order` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL COMMENT '预约单号',
`baby_id` BIGINT NOT NULL COMMENT '宝宝档案id',
`vaccine_id` BIGINT NOT NULL COMMENT '疫苗目录id',
`vaccine_name` VARCHAR(64) NOT NULL COMMENT '疫苗名称(冗余快照)',
`stock_id` BIGINT NOT NULL COMMENT '批次库存id',
`appointment_date` DATE NOT NULL COMMENT '接种日期',
`time_slot` VARCHAR(32) NOT NULL COMMENT '预约时段',
`parent_name` VARCHAR(32) DEFAULT NULL,
`parent_phone` VARCHAR(20) DEFAULT NULL,
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待接种 1已完成 2已取消 3已过期',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`cancel_time` DATETIME DEFAULT NULL,
`vaccinate_time` DATETIME DEFAULT NULL COMMENT '实际接种核销时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_baby_status` (`baby_id`, `status`),
KEY `idx_stock_id` (`stock_id`)
) ENGINE=InnoDB COMMENT='预约订单表';
在设计订单表时,把 vaccine_name、appointment_date、time_slot 这些字段冗余存进来是非常实用的做法。因为预约单生成后,后台即使改了疫苗目录名称或者排班表,家长端“我的预约”页面展示的历史预约记录也不能跟着乱动,这就是典型的“快照”思想,和电商订单里冗余商品名是一个道理。
3.4 状态机设计:预约单每个状态怎么流转
源码里的订单状态看起来只是整型字段,实际上它是一个隐式的状态机。这个状态机的流转规则建议写清楚:
- 状态 0(待接种):创建预约后进入,此时占用批次库存名额
- 状态 2(已取消):家长在接种前取消,或者护士后台操作取消,需要立刻释放批次库存
- 状态 3(已过期):预约日期过了,但没有按时到门诊核销,由定时任务批量从 0 更新为 3,同时释放库存
- 状态 1(已完成):家长到现场,护士确认档案后核销,状态从 0 变为 1,此时释放的是“待接种状态”,但注意已完成不需要恢复库存
设计时需要特别注意的就是“取消/过期必须伴随库存释放”。很多不成熟的项目会遗漏这一步,导致批次显示没有名额,但实际有大量取消订单占着名额,家长约不进来,护士看着后台也一头雾水。
4. 预约主流程实现:从登录到核销的完整链路
4.1 小程序登录与家长身份绑定
微信小程序的登录流程和网页端完全不同。小程序端通过 wx.login() 拿到临时 code,这个 code 有效期只有 5 分钟,传给后端,后端拿着 code + AppID + AppSecret 去微信接口换 openid 和 session_key。openid 是用户在同一个小程序下的唯一标识,后端拿它来关联档案和预约单。
看代码时重点关注这几个方法:
java复制@PostMapping("/api/wechat/login")
public Result login(@RequestBody LoginRequest request) {
// 1. code 换 openid
String url = "https://api.weixin.qq.com/sns/jscode2session?appid="
+ appId + "&secret=" + appSecret + "&js_code="
+ request.getCode() + "&grant_type=authorization_code";
// 2. 查数据库是否存在该 openid 用户,不存在则自动注册
// 3. 生成 JWT token 返回给小程序
}
每次小程序冷启动都会重新调用这个接口,第一次拿到 openid 会顺手创建一条家长用户记录,之后每次登录都只是刷新 token。这个设计比在小程序端强制用户先手机号注册要顺滑得多,用户根本感知不到登录这一步。
4.2 预约创建接口与防重复提交
预约创建是核心中的核心。一个合格的预约接口要做的事包括:判断宝宝档案是否存在、判断疫苗是否符合月龄、检查要预约的批次是否启用、检查该宝宝在相同日期是否已有预约、执行库存扣减、生成预约单。
伪代码如下:
java复制@Transactional(rollbackFor = Exception.class)
public Result createAppointment(CreateAppointmentRequest req) {
// 1. 校验宝宝档案归属
BabyInfo baby = babyMapper.selectById(req.getBabyId());
if (baby == null || !baby.getParentOpenid().equals(currentOpenId())) {
return Result.error("宝宝档案不存在");
}
// 2. 校验疫苗月龄
VaccineCatalog vaccine = vaccineCatalogMapper.selectById(req.getVaccineId());
int ageMonth = calculateAgeMonth(baby.getBirthDate(), LocalDate.now());
if (ageMonth < vaccine.getMinAgeMonth()) {
return Result.error("宝宝月龄未到,暂时不能接种该疫苗");
}
// 3. 防重复预约:同一宝宝同一疫苗只能有一条待接种记录
Long count = appointmentMapper.selectCount(new LambdaQueryWrapper<AppointmentOrder>()
.eq(AppointmentOrder::getBabyId, req.getBabyId())
.eq(AppointmentOrder::getVaccineId, req.getVaccineId())
.in(AppointmentOrder::getStatus, Arrays.asList(0, 1)));
if (count > 0) {
return Result.error("该疫苗已有预约记录,请勿重复预约");
}
// 4. 扣减库存
int rows = vaccineStockMapper.deductStock(req.getStockId());
if (rows == 0) {
return Result.error("该时段号源不足,请选择其他时段");
}
// 5. 创建预约单
AppointmentOrder order = buildOrder(req, baby, vaccine);
appointmentMapper.insert(order);
return Result.ok(order.getOrderNo());
}
扣库存对应的 SQL 是整个系统安全性的屏障:
xml复制<update id="deductStock">
UPDATE vaccine_stock
SET used_count = used_count + 1
WHERE id = #{stockId}
AND used_count < total_count
AND status = 1
</update>
这里的关键在于把“查剩余号源”和“扣减号源”合并成一个原子操作,靠 MySQL 行锁和受影响行数来判断是否扣减成功。返回 0 表示条件不满足,要么已满号,要么批次停用。这样即使同时有十几个人请求同一个批次,数据库也能保证只有批次容量那么多的人扣成功,其他人全部拿到失败提示。
4.3 取消预约与库存释放的写法
取消预约时要注意,只有状态为“待接种”的订单才能取消,已完成的不能取消,已过期的取消也没意义。
java复制@Transactional(rollbackFor = Exception.class)
public Result cancelAppointment(Long orderId) {
AppointmentOrder order = appointmentMapper.selectById(orderId);
if (order == null || !order.getParentOpenid().equals(currentOpenId())) {
return Result.error("预约单不存在");
}
if (order.getStatus() != 0) {
return Result.error("当前状态不允许取消");
}
// 释放库存
int rows = vaccineStockMapper.releaseStock(order.getStockId());
if (rows == 0) {
// 极端情况:库存记录本身被停用/删除,依然需要取消订单
log.warn("释放库存失败,stockId={}", order.getStockId());
}
order.setStatus(2);
order.setCancelTime(LocalDateTime.now());
appointmentMapper.updateById(order);
return Result.ok();
}
后台管理端核销接口的逻辑比较简单:核销人员输入预约单号或者扫家长出示的二维码,将预约单从 0 改成 1,同时记录 vaccinate_time 为当前时间,方便日后追溯。
4.4 定时任务批量处理过期预约
如果家长约了号却没来,系统需要定时清理。这个项目有意思的地方在于直接用 Spring 自带的 @Scheduled 注解就能做到,不需要引入 xxl-job 这类分布式调度框架。
java复制@Component
public class AppointmentExpireTask {
@Scheduled(cron = "0 30 1 * * ?") // 每天凌晨1点半执行
public void processExpiredOrders() {
// 找出所有当天之前仍处于待接种状态的预约单
List<AppointmentOrder> expiredOrders = appointmentMapper.selectList(
new LambdaQueryWrapper<AppointmentOrder>()
.eq(AppointmentOrder::getStatus, 0)
.lt(AppointmentOrder::getAppointmentDate, LocalDate.now()));
for (AppointmentOrder order : expiredOrders) {
// 状态改为已过期并释放库存
order.setStatus(3);
vaccineStockMapper.releaseStock(order.getStockId());
appointmentMapper.updateById(order);
}
}
}
这里有一个小陷阱值得提醒:releaseStock 释放库存时应该写成 used_count = used_count - 1,绝不能写成把 used_count 直接置 0,否则会把其他正常预约的名额覆盖掉,导致后台可预约数变成负值。
5. 小程序端页面与后端联调避坑记录
5.1 小程序页面结构
微信小程序端从功能上拆,需要覆盖以下页面:
| 页面路径 | 说明 |
|---|---|
| pages/index/index | 首页,展示接种须知与入口 |
| pages/baby/list | 宝宝档案列表 |
| pages/baby/edit | 新增/编辑宝宝档案 |
| pages/vaccine/catalog | 疫苗目录与可预约日期 |
| pages/appointment/create | 选批次、提交预约 |
| pages/order/list | 我的预约记录列表 |
| pages/order/detail | 预约详情,展示核销状态 |
如果源码里分了 pages 和 components 两个目录,说明项目组有意识地把可复用的列表项、空状态组件抽出来了,这对小程序这种“每次 setData 都得考虑性能”的环境其实是好习惯。
5.2 小程序请求封装与 token 注入
小程序端不能像浏览器一样自动携带 Cookie,所以每次请求都要手动把 token 放到 header 里。正规做法是在 app.js 或者单独 utils/request.js 里封装一层 wx.request,在成功回调里统一判断业务状态码。
javascript复制const request = (url, method, data) => {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.request({
url: getApp().globalData.baseUrl + url,
method: method || 'GET',
data: data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? 'Bearer ' + token : ''
},
success: (res) => {
if (res.data.code === 200) {
resolve(res.data.data);
} else if (res.data.code === 401) {
// token 过期,重新走登录流程
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
reject(res.data);
} else {
wx.showToast({ title: res.data.message, icon: 'none' });
reject(res.data);
}
},
fail: (err) => {
wx.showToast({ title: '网络请求失败', icon: 'none' });
reject(err);
}
});
});
};
注意小程序的 wx.request 并发请求默认限制是 10 个,虽然社区预约场景不太可能触发这个限制,但批量查询档案和疫苗目录时如果接口设计不好,还是会出现页面转圈很久才能看到数据。建议首页并行加载不超过 3 个接口,其他数据懒加载。
5.3 真机联调最容易踩的三个坑
第一个坑是开发者工具能访问后端,但手机预览时请求失败。原因是开发者工具有“不校验合法域名”的开关,而真机上必须在小程序后台配置 request 合法域名才能发起请求。如果后端还在本地开发,用了局域网 IP,那就必须在开发者工具里开启“不校验合法域名”并勾选真机调试模式,否则手机会因为域名校验失败直接报 url not in domain list。
第二个坑是后端接口返回的日期时间格式不统一。小程序端解析 2025-06-14T08:30:00 这种带 T 的格式在某些 iOS 低版本上会显示为 NaN。解决办法是在后端统一配置 Spring Boot 的 Jackson 序列化格式:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
第三个坑是后端返回的 LocalDate 类型默认序列化成数组,类似 [2025, 6, 14],小程序端取不到年月日导致页面渲染异常。处理办法是在实体类的 LocalDate 字段上加 @JsonFormat(pattern = "yyyy-MM-dd"),或者在配置类里注册 JavaTimeModule 并设置格式化策略。
5.4 如何给家长做到苗提醒
完整的社区疫苗预约体验不应当停留在“预约”这一步,到苗提醒和接种前提醒可以显著提高履约率。微信小程序里通知用户最常见的方式是订阅消息,家长授权后可以由后端在接种日前一天通过微信服务端接口下发提醒,这个交互流程源码里通常不会完整实现,但如果做二开,可以在预约成功时引导家长点击订阅授权按钮。
javascript复制wx.requestSubscribeMessage({
tmplIds: ['接种提醒模板ID'],
success(res) {
// 用户同意后,后端存下该 openid 的订阅关系
}
});
接种日前一天系统调用 https://api.weixin.qq.com/cgi-bin/message/subscribe/send 下发消息,内容可以带上宝宝姓名、疫苗名称、预约时段。这个功能上线后就解决了我前面说的“护士挨个打电话通知”的痛点,家长爽约率能明显降下来。
6. 编译报错、运行异常排查与上线建议
6.1 拿到源码后编译运行最常见的几个问题
| 报错/现象 | 原因 | 处理方式 |
|---|---|---|
java: 找不到符号 符号: 方法 getParentOpenid() |
Lombok 插件缺失或未启用 | IDE 安装 Lombok 插件,开启 Annotation Processing |
Access denied for user 'root'@'localhost' |
数据库账号密码不对 | 修改 application.yml 数据源配置 |
Unknown database 'vaccine' |
未创建数据库 | 执行 CREATE DATABASE vaccine DEFAULT CHARACTER SET utf8mb4 |
The server time zone value '�й���ʱ��' is unrecognized |
MySQL 时区不识别 | JDBC 连接串加 serverTimezone=Asia/Shanghai |
Invalid bound statement (not found) |
XML Mapper 扫描路径不对 | 检查 mapper-locations 配置和 XML 文件位置 |
Port 8080 was already in use |
端口被占用 | 改 server.port 或关掉占用进程 |
这些坑排完后,正常启动会看到 Spring Boot 的 banner 和一行 Started VaccinationApplication 日志,之后就可以用 Swagger 或者直接 Postman 调用接口测试了。
如果连生成 banner 都觉得费劲,可以搜一下“Spring Boot banner 生成器”这种在线小工具,把生成的 ASCII 艺术字复制到 src/main/resources/banner.txt,每次启动都显得很专业。当然这只是锦上添花,不影响业务。
6.2 业务数据上的隐蔽 bug
编译问题好解决,业务逻辑上的隐蔽 bug 才是最坑的。我举几个实际容易出现的情况。
第一个是同一宝宝两个疫苗同一天接种的冲突。比如宝宝 6 月龄时要同时打乙肝第三剂和流脑第一剂,两个是不同的疫苗,如果用“同一宝宝同日只能一条预约”去卡,就会把真实需求误杀。这个项目的设计里通常会把这种场景交给批次库存去天然区隔,让家长分别为两个疫苗预约不同时段,也算说得通。如果要做得更严谨,可以在创建预约时检查同一天所有预约单的时段重叠情况。
第二个是换手机或者清除微信缓存后 openid 不变,但 token 失效导致的“登录态掉了”。处理方式是拦截器里发现 token 解析失败,直接返回 401 状态码,小程序端收到 401 后静默重新调用 wx.login 换取新 token,而不是弹窗让用户手动登录。
第三个是后台管理员的权限边界。源码如果不做用户角色表,那“核销预约”和“维护疫苗库存”的接口就必须至少做一层管理员账号密码校验。很多项目会直接在数据库中塞一条 admin 记录,登录后返回管理员 token,和普通家长 token 混用,这种做法只能算应急,不推荐生产环境长期使用。
6.3 项目本身可以扩展的方向
如果你是拿这个源码做毕业设计或者学习 Spring Boot,建议不要停留在跑通的层面。可以考虑往里面加这三个方向的东西:
引入 Redis 做高频疫苗目录的缓存,降低 MySQL 压力,同时把“同一手机号每天最多预约 2 次”这类频控规则用 Redis 计数器实现;管理端从“只有核销功能”扩展为 Vue 或 React 的独立后台系统,加入疫苗批次创建、日报统计、接种率图表;如果希望流程更复杂,可以尝试引入 Flowable 工作流引擎来编排“预约申请→门诊审核→接种登记→留观结束”的完整流程,不过对于这种社区规模的应用其实用状态机就足够了,Flowable 更多是学习价值。
6.4 上线前需要检查的清单
功能跑通之后,上线前有几件容易被忽视的小事。把 application.yml 里的数据库连接从 root 改成独立账号,密码加密存储,避免源码泄露导致数据库裸奔;Spring Boot 端口不要用默认的 8080,最好换一个不常用的端口,服务器上配合 Nginx 反向代理并配置 HTTPS 证书;小程序端发布前把 request 合法域名替换成正式域名,开发者工具里的“不校验合法域名”关闭后再走一遍完整流程;预约数据涉及新生儿出生日期和联系方式这些个人信息,生产环境建议梳理一遍权限,避免越权接口查询他人信息。
7. 源码里最值得反复品味的几个细节
说白了,这个项目真正值钱的地方不在于功能有多花哨,而在于它把很多实际业务中必须考虑的东西做进去了。vaccine_stock 表用批次 + used_count 而不是简单靠程序判断剩余名额,这是高并发场景的经典解法;预约单冗余疫苗名称和接种日期,是订单设计里常用的快照思路;取消预约和过期释放库存做成事务,是为了保证计数的一致性。
我个人在实际操作中的体会是,社区类预约项目没有太难的技术点,难的是把业务规则盘清楚——什么时候能约、什么时候算过期、取消后库存怎么还回来、保留哪些历史展示字段。这套源码把这些规则落到了具体的表结构和代码逻辑里,你如果能跟着走一遍,后面自己写任何带库存扣减的业务都会顺手很多。唯一要提醒的就是别照着抄完就完事,生产环境里数据量一旦上来,公告表、管理员角色表、操作日志表都是迟早要补上的。总之先跑起来,跑通了再一步一步迭代,这个方向不会错。
