简介:这是一套面向Java后端与微信小程序全栈开发者的疫苗预约接种系统实战源码,适用于课程设计、毕业设计及医疗类小程序项目参考。系统采用Spring Boot + Vue + 微信小程序技术栈,覆盖管理员与接种者双角色全流程业务,包括接种点管理、预约计划调度、疫苗信息维护、支付模拟、二维码签到及多维度历史查询等核心功能。压缩包共644个文件,含86个Java后端逻辑文件、115个Vue前端组件、119个HTML页面、86个XML配置文件及75个GIF动效资源,整体3.56MB,结构清晰、模块解耦,便于二次开发与教学演示。已有1054人学习下载,配套application.properties数据库配置说明与MySQL初始化SQL脚本,开箱即用;内容预览显示包含Layui、Layer等主流前端UI库及完整登录/管理/预约模块样式体系,具备真实生产环境适配能力。
1. 疫苗预约项目的需求解剖:这系统不是简单做个表单
1.1 疫苗预约和普通挂号的本质区别
做疫苗预约接种系统之前,我一度以为这跟做学校预约、体检预约没什么区别,无非就是用户选一个时间、提交表单、后台记一条数据。真正开始梳理需求才发现,疫苗预约比普通预约复杂得多,核心区别在于两件事:稀缺号源的并发分配,以及多针次疫苗的接种进度跟踪。
普通挂号,医生每天看几十个号,你抢不到可以换一个医生,资源是冗余的。疫苗不一样,很多疫苗(比如九价HPV、流感疫苗)到货数量有限,每个接种点放出的号源是固定的,抢完就没了。而且像HPV九价这种疫苗要打三针,第二针、第三针的时间间隔有严格限制,系统必须能记录用户已经打到第几针,避免提前或延后接种。更别说儿童疫苗里还有一类苗和二类苗的区别,有些疫苗需要跟上次接种间隔28天以上,这些规则如果全靠人工判断,接种点的工作量会大得吓人。
所以这套基于微信小程序的疫苗预约接种系统,表面上解决的问题是"用户在小程序里选时间、提交预约",实际要解决的是三个更深层的问题:
- 在不同人群同时抢同一个号源时,怎么保证不超卖、不重复。
- 怎么让用户和接种点都能清晰地看到一条接种时间线,尤其是多针次疫苗。
- 怎么减轻接种点工作人员的人工核对压力,把"核销、确认、记录"变成一套标准流程。
搞清楚这三点,才知道功能清单该怎么划。如果你只是照着网上的CRUD模板写,最后做出来的东西大概率只能在演示视频里跑,上了真实环境就是事故现场。
1.2 业务状态机:一次接种要经历哪些状态
预约系统的难点一定不在增删改查,而在状态流转。我设计状态机的时候专门画了一张流转图理思路,这里用文字描述:
一次预约从用户提交开始,需要经过以下状态:
待接种(已预约):用户在小程序里提交预约成功,此时号源已锁定,占用一个名额。
已取消:用户在接种时间之前主动取消,号源释放回池子,其他用户可以继续预约该时段。
已完成(已接种):用户到达接种点,工作人员核销预约码,确认接种完成。此时这个号源才算真正消耗掉。
爽约/未核销:预约时间到了但用户没来,而且没有提前取消,预约失效。每个用户如果多次爽约,可以考虑限制后续预约权限,这个后面细说。
已过期:预约时间已经过去,但用户既没有取消、也没有接种,系统自动从"待接种"置为"已过期"。
这里有个容易踩坑的点:状态字段到底用几个数字表示。看到很多项目喜欢把状态定义成0/1/2这种纯数字,然后在前端用switch-case去映射文案。前期确实省事,但后面加需求(比如"已过期"和"爽约"其实是两种语义)的时候就非常痛苦。我建议一开始就用字符串枚举,比如PENDING、CANCELLED、VACCINATED、NO_SHOW、EXPIRED。虽然数据库稍微多占一点空间,但代码可读性和可维护性完全是两个级别。
1.3 功能模块的边界划分
整个系统我拆成了三个端:
用户端(微信小程序):微信授权登录、疫苗列表、按接种点查看号源、选择接种人、提交预约、预约记录列表、取消预约、接种档案查询。
接种点端(管理后台网页):号源配置(放号数量、时间)、预约列表查询、核销确认、接种记录补录、简单的数据统计。
系统端(管理后台高权限):管理员管理、接种点管理、疫苗管理、针对全量预约数据的统计。
小程序端不需要做得太臃肿,核心就是把"找疫苗-选时间-提交预约-查看记录"这条链路走通。管理后台反而是工作量的大头,因为接种点工作人员的真实操作场景非常杂,他们需要在电脑上快速搜索一个用户的预约记录,需要扫码核销,需要手动处理一些异常情况(比如用户到现场了但没预约成功)。
我当时给管理后台定的原则是:小程序的每个用户操作,后台都能看到对应的数据;后台的每次核销操作,都会推动预约状态向前流转。这条原则保证了两个端的数据不会各说各话。
2. 技术选型与数据库设计:前后端怎么搭更稳
2.1 为什么选原生微信小程序 + SpringBoot 这对组合
技术选型没有绝对的对错,只有合不合适。这套系统我选择了原生微信小程序 + Spring Boot + MySQL + Redis + MyBatis-Plus。下面说说我为什么这么选。
前端没用uni-app,是因为项目本身就是给微信生态用的,没有多端诉求,用原生框架最直接,调试起来也最方便。虽然网上经常看到"uniapp做微信小程序在手机上预览没问题、但在微信开发者工具里白屏"这类问题,原生小程序虽然也要处理兼容性,但至少少了一层框架的转译开销,定位问题相对直接。
后端用Spring Boot,是因为团队对Java最熟,而且Spring Boot对于这种典型的管理系统非常稳妥,生态完善。MyBatis-Plus用来简化单表操作,但复杂查询还是手写SQL,这里不建议完全依赖MP的Wrapper去做多表关联,维护起来会想哭。
Redis在整个项目里承担两件事:第一是分布式锁,用来解决号源并发扣减的问题;第二是缓存号源列表和疫苗列表,降低数据库压力,这个场景QPS虽然不算高,但接口响应速度确实能提升不少。
如果你一个人开发、不想折腾Java环境,那用Node.js+Express或者PHP也能做。我见过不少用PHP做的预约系统,跑得也很稳。关键还是你的业务逻辑设计,技术栈只是载体。
2.2 数据库表结构设计:核心表并不复杂,但字段足够多
数据库设计是这类系统最值得花时间的地方。我拆出下面这几张核心表:
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar(64) | 微信openid,唯一 |
| nickname | varchar(50) | 昵称 |
| phone | varchar(20) | 手机号 |
| id_card | varchar(18) | 身份证号,实名档案用 |
| created_at | datetime | 注册时间 |
疫苗表(vaccine)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(50) | 疫苗名称 |
| manufacturer | varchar(50) | 生产厂家 |
| dose_count | int | 需要接种的针次(1/2/3) |
| interval_days | int | 针次间隔天数,简化的校验规则 |
| description | text | 适用人群、注意事项 |
| status | tinyint | 上下架状态 |
接种点表(site)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(100) | 接种点名称 |
| address | varchar(200) | 地址 |
| phone | varchar(20) | 联系电话 |
| work_time | varchar(100) | 服务时间描述 |
号源时段表(appointment_slot)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| site_id | bigint | 接种点ID |
| vaccine_id | bigint | 疫苗ID |
| slot_date | date | 预约日期 |
| start_time | varchar(10) | 开始时间,如 09:00 |
| end_time | varchar(10) | 结束时间,如 09:30 |
| total_count | int | 总号源数 |
| booked_count | int | 已预约数 |
| version | int | 乐观锁版本号 |
预约表(appointment)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 预约单号,唯一 |
| user_id | bigint | 用户ID |
| patient_name | varchar(50) | 接种人姓名 |
| patient_id_card | varchar(18) | 接种人身份证 |
| patient_phone | varchar(20) | 接种人电话 |
| vaccine_id | bigint | 疫苗ID |
| site_id | bigint | 接种点ID |
| slot_id | bigint | 号源时段ID |
| dose_no | int | 第几针 |
| status | varchar(20) | PENDING/CANCELLED/VACCINATED/NO_SHOW/EXPIRED |
| cancel_reason | varchar(200) | 取消原因 |
| vaccinated_at | datetime | 实际接种时间 |
| created_at | datetime | 创建时间 |
这套表设计谈不上惊艳,但每一张表都有它存在的理由。有几个关键点值得多说一句。
第一,预约表里有patient_name、patient_id_card这几个冗余字段,而不是只存user_id然后去关联用户表。因为成人预约经常是给孩子或者父母预约,接种人和小程序登录用户不是同一个人。如果你想在预约表里只存一个 "预约人ID",就得再加一张家属表,复杂度会上升不少。这里直接用冗余字段反而简单。
第二,号源时段表加了version字段做乐观锁,这是并发控制的基石。具体用法在后面代码部分展开。
第三,预约单号order_no一定要独立出来,不要用自增ID给用户看。一是避免暴露系统业务量,二是方便后续对接短信、对账等场景。
2.3 号源数据是怎么生成的
号源配置是整个系统我最满意的一个设计。管理后台里,工作人员只需要选择疫苗、接种点、日期范围、每天放多少号,系统就会自动在appointment_slot表里批量生成号源记录。
这里有一个细节:如果放号日期是固定的"未来N天滑动窗口",就不需要一次性生成半年的数据。比如管理员设定"提前7天放号",系统每天凌晨定时任务跑一次,检查未来第7天的号源是否存在,不存在就生成当天的号源。这样数据库里永远是最近7天+的号源数据,不会出现用户看到一个月后的号源但管理员根本来不及调整配置的情况。
这个设计在代码里就是一条批量插入语句,用MyBatis的foreach循环插入即可。需要关注的不是插入逻辑,而是定时任务的时间点要设在凌晨低峰期,避免影响白天的查询性能。
3. 小程序端核心功能实现:预约流程的完整链路
小程序端我重点讲登录、疫苗列表、号源选择、提交预约、订阅消息这五个环节。
3.1 登录态处理:wx.login 到 openid 再到 token
微信小程序不能直接拿用户的openid,必须通过wx.login()拿到临时code,然后由后端调用微信接口换取。这个流程很多人第一次接触容易绕晕,我把它拆成三步:
第一步,小程序端调wx.login()拿到 code。 第二步,把 code 传给后端接口/api/auth/login。 第三步,后端拿着 code 去请求微信的code2Session接口,换取 openid 和 session_key,然后返回一个自定义的登录态 token 给小程序。
这个 token 我用的是JWT,里面只放userId和openid,有效期设为7天。小程序前端把 token 存到wx.setStorageSync('token', token),之后每次请求都在 header 里带上。
// utils/request.js const BASE_URL = 'https://api.example.com' const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') }, success(res) { if (res.statusCode === 200) { resolve(res.data) } else if (res.statusCode === 401) { // token过期,重新登录 wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(res.data) } }, fail(err) { reject(err) } }) }) } module.exports = { request }登录页面里,注意wx.getUserProfile改版后的授权逻辑,现在不能直接弹窗拿用户头像昵称了,我建议不要卡在这一步,先让用户能登录能预约,头像昵称可以后续在"个人中心"里引导用户补充。初始昵称直接用"微信用户"兜底。
3.2 疫苗列表与号源展示
疫苗列表页面就是一个典型的列表页,后端接口返回疫苗名称、厂家、价格(如果有)、剩余号源总数。这里我刻意做了一个聚合查询,避免前端拿到疫苗列表后再逐条去请求号源数量。
// VaccineController.java @GetMapping("/vaccine/list") public Result listVaccines() { List<VaccineVO> list = vaccineService.listWithSlotCount(); return Result.success(list); }对应的SQL大致是:
SELECT v.id, v.name, v.manufacturer, v.dose_count, v.description, COALESCE(SUM(s.total_count - s.booked_count), 0) AS remaining_count FROM vaccine v LEFT JOIN appointment_slot s ON s.vaccine_id = v.id AND s.slot_date >= CURDATE() AND s.booked_count < s.total_count WHERE v.status = 1 GROUP BY v.id这个查询把"还有没有号"一次性算出来,前端拿到疫苗列表就能直接展示"剩余N个号",体验很好。
点击某个疫苗进去,用户先选择接种点,再选择日期。日期我建议用小程序原生的<picker mode="date">,但要注意start和end参数的动态设置。选好日期后,再调号源列表接口,把当天的所有可约时段展示出来。
// pages/slot/slot.js const getSlots = async () => { const res = await request(`/api/slot/list?vaccineId=${vaccineId}&siteId=${siteId}&date=${date}`) setData({ slots: res.data }) }每个时段卡片上显示09:00-09:30(剩余8个号),如果booked_count >= total_count,这个时段的卡片置灰不可点。这里用了表单radio-group或者自由定制的单选框样式——注意微信小程序的 radio 组件默认样式丑,而且不同机型上尺寸有差异,建议直接用 view 配合选中态样式来做单选项。这也是热搜上"微信小程序单选框"高频出现的原因,很多人卡在自定义样式上。
3.3 提交预约的并发控制:不超卖的保障
这是整个项目技术含量最高的一块。用户选好时段、填写接种人信息后,点击提交预约,后端要做的是:扣减号源、创建预约记录,必须在一个事务里完成,而且不能出现超卖。
我选了乐观锁方案,适用于这种绝大多数情况不会冲突、但极端情况下必须保证正确的场景。
// AppointmentServiceImpl.java @Transactional(rollbackFor = Exception.class) public void createAppointment(CreateAppointmentRequest req) { // 1. 校验用户是否已有同疫苗未完成预约 Integer pending = appointmentMapper.countPending(req.getUserId(), req.getVaccineId()); if (pending != null && pending > 0) { throw new BizException("您已有待接种的预约,请勿重复预约"); } // 2. 扣减号源,乐观锁控制并发 int updated = slotMapper.decreaseStock(req.getSlotId()); if (updated == 0) { throw new BizException("手慢了,该时段号源已被约满"); } // 3. 创建预约记录 Appointment appointment = new Appointment(); appointment.setOrderNo(OrderNoGenerator.generate()); // ... 设置其他字段 appointmentMapper.insert(appointment); }关键是第2步的SQL:
UPDATE appointment_slot SET booked_count = booked_count + 1, version = version + 1 WHERE id = #{slotId} AND booked_count < total_count AND version = #{version}这个SQL的意思是:只有当前booked_count小于total_count时,才允许把booked_count加1。数据库层面的行锁天然保证了并发安全,两个用户同时提交时,只有一个用户能成功执行这条UPDATE,另一个的updated返回0,然后抛异常提示"已约满"。
有人会问:为什么不直接用Redis的INCR做预扣?Redis方案在高并发下性能确实更好,但需要额外处理Redis和数据库的一致性问题,比如用户预约成功后Redis扣了,但创建预约记录时数据库报错,Redis的数字又得回滚,这在业务上非常容易出Bug。对于预约系统这种并发量(几百人抢几十个号)的场景,数据库乐观锁完全够用,而且逻辑简单、容易排查。不要为了炫技引入不必要的复杂度,这是做业务系统最重要的原则。
还有一个小细节:用户重复预约的校验。我做了两层,一层是上面代码里的countPending数据库查询,另一层是给appointment表加了一个联合唯一索引(user_id,vaccine_id,status),不过MySQL的联合唯一索引对status这种会变的值不太好使,所以实际还是要靠事务内的查询+插入来保证。真正高并发的场景可能还需要Redis做前置拦截,但在这个量级下,数据库查询就能扛住。
3.4 订阅消息通知:让用户知道约没约上
预约成功后,我给用户发送一条微信订阅消息,告知预约成功、接种时间、接种地点。这一步需要在用户点击"提交预约"之前主动调wx.requestSubscribeMessage请求授权。
// pages/confirm/confirm.js const subscribeMessage = async () => { const tmplIds = ['订阅消息模板ID'] return new Promise((resolve) => { wx.requestSubscribeMessage({ tmplIds, success(res) { resolve(res[tmplIds[0]] === 'accept') }, fail() { resolve(false) } }) }) }一个容易被忽略的点:订阅消息分为一次性订阅和长期订阅,普通开发者申请到的模板基本都是"一次性订阅"——用户授权一次,你只能发送一条消息。所以不要在用户点击提交之前就把授权次数消耗掉了。我这里是用户点了"提交预约"后,先弹授权,用户同意后调登录、提交预约接口,预约成功后再发订阅消息。
后端发送订阅消息的逻辑不复杂,难点在于access_token的获取和缓存。微信的access_token有效期是2小时,频繁获取会被限流,一定要把access_token存到Redis里,设置过期时间7200秒,快过期再重新获取。我见过不少项目没做缓存,请求稍微一多就报45009接口调用超限,这种错属于完全可避免的。
4. 管理后台的号源与核销设计:工作量的大头
4.1 放号配置与每日定时任务
管理后台我用的是Vue3 + Element Plus,和SpringBoot后端通过 RESTful 接口对接。页面上维护"放号规则",以某一种疫苗在某个接种点为单位,配置放号周期、每日号源总数、时段拆分规则(比如上午9点到11点,每个小时为一个时段,每个时段放10个号)。
前端的表单提交后,后端生成号源数据的逻辑大致是:
// SlotJob.java @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void generateSlots() { List<SlotRule> rules = slotRuleMapper.selectAll(); for (SlotRule rule : rules) { LocalDate targetDate = LocalDate.now().plusDays(rule.getAdvanceDays()); int count = slotMapper.countByDateAndRule(rule.getId(), targetDate); if (count == 0) { slotService.generateForDate(rule, targetDate); } } }这里generateForDate内部会根据规则里的时段列表,批量插入多条appointment_slot记录。要注意批量插入不要循环单条插入,一定要用MyBatis的<foreach>拼成一条INSERT执行,性能差好几倍。
另外,放号规则里通常还要配一个"放号时间"的概念,比如每天上午10点放第7天的号。这个时间如果在凌晨定时任务里实现比较别扭,因为用户能看到号源的时间不一定是数据生成的时间。我的做法是:凌晨2点只生成"不可见"的号源数据,然后提供一个openSlots操作,由管理后台的定时任务在指定时间(比如10:00)把号源的状态从0改为1。前端查询时只返回status=1的号源。这样既灵活又可控。
4.2 核销流程:扫码和手动核销两条路
用户到了接种点,工作人员登录管理后台,首页默认显示当天的全部预约列表。核销有两种方式:
第一种是手动查询:工作人员输入用户的预约单号或手机号,查出预约记录,确认状态是PENDING,点击"确认接种",状态变为VACCINATED,同时记录vaccinated_at。
第二种是扫码核销:用户小程序端"我的预约"页面会展示一个预约二维码(我用的是小程序码,二维码里带order_no),工作人员在后台用扫码枪扫一下,前端拿到单号后调后端核销接口。这里扫码枪本质就是一个USB输入设备,扫到的内容会以键盘输入的方式自动填入输入框,所以我只需要监听输入框的keyup事件,回车后自动触发搜索,并不需要什么高级硬件对接。
核销接口有一个细节:核销和状态变更要校验当前状态。
// AppointmentController.java @PostMapping("/api/appointment/vaccinate") public Result vaccinate(@RequestBody VaccinateRequest req) { int updated = appointmentMapper.updateStatus( req.getOrderNo(), "PENDING", "VACCINATED", LocalDateTime.now()); if (updated == 0) { throw new BizException("该预约已处理或状态异常,请刷新后重试"); } return Result.success(); }UPDATE ... WHERE order_no = ? AND status = 'PENDING'这种写法,能防止工作人员连续点两次按钮导致状态被重复更新。虽然不是高并发,但我们要养成这种防御性编程习惯。实际核销过程中会遇到各种意外,比如用户明明没预约成功但拿着一张取消状态的记录来现场,这时候后台要能查到取消原因,工作人员可以正常沟通处理。
4.3 统计报表:不用太花哨,但要能看出问题
管理后台的统计模块我做得很克制,只做了四个指标:今日预约数、今日核销数、今日取消数、各疫苗剩余号源数。查询逻辑完全是COUNT+GROUP BY,没有用任何复杂的大数据组件。
为什么做这么简单?因为实际上接种点最关心的三个问题是:"今天约了多少人""来了多少人""哪些疫苗快没号了"。你给一个接种点看折线图、地图分布,他们反而无所适从。做业务系统,报表的复杂程度一定要跟着真实使用场景走。
有一点很多人会忽略:统计数据里要区分"爽约"和"已过期"。爽约意味着用户预约了但没来,这说明有资源被浪费了;已过期可能是因为号源过期时间设置太短。如果发现某个时段爽约率特别高,就可以提醒管理员调整放号策略,比如把号源时间从半小时改为15分钟,减少单时段资源堆积。
5. 上线前后踩过的坑:小程序审核与真机调试
5.1 小程序类目审核:没资质怎么办
做医疗相关的小程序,微信审核非常严格。疫苗预约这个领域,你选择"医疗-医疗服务"类目,平台会要求提供医疗机构执业许可证等资质。如果你只是个人开发者或者手里没有医疗机构资质,这条路基本走不通。
我实际踩下来的经验是:如果你的系统只是做预约工具、不涉及在线问诊、不涉及处方药销售,可以尝试选择"工具-预约/报名"类目。类目不同,审核要求差异很大,但前提是你的预约功能里不能出现"疫苗""接种"这类强医疗词汇。这需要你在系统名称、页面文案上做一些调整,比如统一叫"预约服务""健康服务"。
这里提醒一句:千万不要为了过审去伪造资质,小程序审核会定期复核,一旦被发现轻则下架、重则封号。如果确实是真实的医疗机构项目,就直接用机构资质去申请,一步到位。
还有一个相关的坑:如果预约流程里涉及在线支付(比如二类苗付费),小程序平台对虚拟支付有严格限制。疫苗是实物服务,问题不大,但如果你涉及到的是"在线问诊+支付",那就要小心了。我的建议是:疫苗预约系统尽量不要在小程序内做线上支付,让用户到现场缴费,省掉一堆审核和资金合规的麻烦。
5.2 真机正常但开发者工具白屏:别慌,按顺序排查
开发过程中我遇到过一个小程序在手机上预览没问题、但在微信开发者工具里打开白屏的情况。排查一圈下来,原因竟然是最简单的:开发者工具的基础库版本太旧,不支持我在代码里用到的某个新API。更新基础库版本后,一切正常。
这类问题排优先顺序的建议是:先看开发者工具的Console报错,再看是否用了新API,再看组件是否在页面json文件里注册,最后看app.json的页面路径是否正确。白屏问题里十有八九是JavaScript报错打断了渲染,而报错信息往往被开发者工具藏在了Console面板里,一眼没看见就慌了。
另外,模拟器和真机的样式差异非常普遍。我经常遇到模拟器上好好的,真机上某个margin多出来几个像素,或者flex布局产生换行。这个没有捷径,只能在真机预览里过一遍核心页面。特别是微信小程序顶部导航栏的高度问题,不同机型状态栏高度不同,如果你做了自定义导航栏,一定要用wx.getSystemInfoSync()获取statusBarHeight,再根据胶囊按钮位置计算导航栏高度,固定写死90px在这个年代已经不现实了。
5.3 HTTPS、域名白名单和抓包调试
小程序的wx.request强制要求HTTPS,而且域名必须在小程序后台配置为白名单。开发阶段可以在开发者工具里勾选"不校验合法域名",但上线前一定要把正式域名的SSL证书配好,否则真机一上线就是页面白屏、请求全部失败。
关于调试,很多人会问小程序怎么抓包。我的做法是:开发者工具里勾选"打开调试",然后通过代理工具(比如Charles或Reqable)设置HTTPS代理,手机和电脑连同一个WiFi,手机WiFi代理指向电脑IP,再安装代理的根证书就能看到小程序的所有请求。注意微信iOS端的证书信任逻辑和Android不完全一样,iOS需要到设置里手动信任证书,这一步经常有人漏掉导致抓不到包。
抓包的场景非常关键,尤其是上线后发现某个用户预约失败,你需要在后台看他的完整请求链路:是登录接口返回了401,还是号源扣减失败,还是参数校验没过。有了抓包工具,这些问题几分钟就能定位,否则只能靠用户截图,效率极低。
6. 这套系统的通用化思路与实际效果
6.1 从疫苗预约到通用预约:改哪些地方就能复用
项目做完之后我回头审视了一遍,发现这套系统里大概70%的代码是完全通用的,只需要把"疫苗"这种业务概念抽象成"资源",就能复用到很多预约场景。
你只需要改这几个地方:
第一,把vaccine表换成语义更通用的resource表,字段从"疫苗名称、厂家、针次"改成"资源名称、描述、单位"。
第二,把dose_no(第几针)的逻辑去掉。如果预约的是一次性资源(比如场地预约、体检预约),这个字段就冗余了;如果是多阶段资源(比如驾考科目二约了还要考科目三),可以把它泛化成step_no。
第三,把接种人信息改成动态表单。疫苗预约场景的"接种人姓名、身份证、电话"是固定的,但通用预约里不同资源要收集的信息不一样。可以用JSON字段存表单数据,或者用一张扩展字段表,让管理员在后台自定义需要收集哪些字段。
剩下的登录、号源管理、并发控制、核销、统计、订阅消息通知,全部不用动。如果你接下来还想做"预约"相关的项目,直接从这套骨架上改是最快的方式。
6.2 性能数据和实际使用反馈
系统上线后,我用压测工具简单跑了一下号源提交接口,模拟30个用户同时抢一个只有10个号源时段的场景,最终成功预约数为10,剩余20个请求全部返回"号源已约满"。数据库没有任何死锁,接口P99响应时间在200ms以内,对于这种业务场景来说完全够用。
实际运营中最让我意外的不是并发,而是用户取消预约的频次。上线第一周,当天预约的取消率竟然超过了15%。这直接导致的问题是:有些时段白天看着已经约满,晚上用户一取消,第二天早上又放出几个号,但此时已经没有用户去刷了,号源就白白浪费掉。
针对这种情况,我加了一个很简单的小功能:取消预约的号源不会立即释放,而是进入一个"待释放"状态,在每天凌晨定时任务里统一释放。这样第二天用户打开小程序看到的是重新整理过的号源分布,而不是乱七八糟的零星空位。实测下来,号源利用率提升了大概10%左右。
6.3 几个我认为最值得记住的经验
回头整理这个项目,有几个经验如果让我重做一遍依然会坚持:
第一,状态字段用字符串枚举,不要用数字。项目维护到后期,status=0到底是"已取消"还是"待确认",你根本记不住,每次都要翻代码注释。字符串一眼就能看明白。
第二,并发控制用数据库乐观锁就好,别一上来就上消息队列和分布式事务。预约系统的并发量级远没有达到需要那套重型方案的级别,而且那套方案会引入大量不可控的中间件故障点。等你真的需要处理每秒上万次请求时,再演进架构,没有迟。
第三,给用户看的通知一定要"准"。订阅消息看似只是一个小功能,但"预约成功没通知"和"预约成功通知内容写错了时间"是完全不同的两个问题。后者会让用户白跑一趟,投诉率飙升。发送逻辑里一定要用预约记录里的真实字段,不要用前端传过来的时间,避免数据不一致。
第四,一定要在开发阶段就把日志打好。我项目里每个预约状态变更都写了@Slf4j日志,包含orderNo、旧状态、新状态、操作人、时间。上线后排查问题,这些日志就是救命稻草。没有日志的情况下去排查并发问题,等于盲人摸象。
这套系统从需求梳理到上线,前后花了大概三周,如果说最核心的收获,不是代码写了多少行,而是把"预约"这件事的底层逻辑真正想清楚了:号源是稀缺资源、状态是流转的、用户和工作人员看到的永远应该是同一份数据。你如果也在做类似的项目,建议先把业务状态机画明白,再动手写代码。状态清楚了,页面和接口只是体力活。
本文还有配套的精品资源,点击获取