简介这是一套基于微信小程序的疫苗预约接种系统源码面向需要完成课程设计、毕业设计或二次开发练手的学生与开发者。项目对原有疫苗预约系统进行了重构采用前后端分离结构导入IDEA后修改pom中的MySQL驱动及application.properties里的数据库与图片路径即可启动运行。系统分为管理员与接种者两端管理员可查看数据分析图并对接种点、医护人员、预约计划、疫苗及接种者信息进行增删改查同时查询支付、预约、签到、预检、接种、留观等历史记录接种者可浏览疫苗与接种点列表、查看预约计划、提交预约申请、模拟支付并查看接种二维码状态。压缩包共644个文件约3.56MB包含86个Java后端源码、115个Vue页面、119个html模板、86个xml配置及js、scss、sql、properties等资源结构完整。目前已有1054人学习下载适合作为小程序与后台管理系统的实战参考。1. 疫苗预约接种小程序从一份源码包到能跑通的业务闭环社区接种点每天放号两百个早上八点开放八点零三分全部约满剩下的是电话打不进来、现场排长队、护士手工登记身份证号。这个场景催生了「基于微信小程序的疫苗预约接种系统源码」这类需求——它要解决的核心问题就三件事把号源放到线上、把身份核验做在预约环节、把接种记录沉淀成可查询的数据。源码包本身不是终点能不能在本地跑起来、能不能接上自己的数据库、能不能改出符合本地接种流程的规则才是决定这套东西值不值得投入的关键。这篇文章面向两类人一类是手里已经拿到源码包、想快速跑通并二次开发的开发者另一类是准备从零搭一套类似系统的技术负责人想先看清整体结构和坑位再决定自研还是改现成。我会按「先跑通最小闭环 → 再拆模块 → 再讲参数和避坑 → 最后落到可验证的进阶技巧」的顺序展开涉及微信小程序登录、预约时段控制、号源并发扣减、接种记录查询这几个必做模块。源码包里的具体文件结构以你实际拿到的为准我讲的是这类系统的通用骨架和落地路径。2. 先跑通最小闭环登录、选时段、提交预约2.1 微信小程序登录态与手机号获取的对接方式任何预约系统的第一步都是确认「谁在约」。微信小程序的登录链路是固定的前端调wx.login拿临时 code后端拿 code 加 AppID、AppSecret 去换 openid 和 session_key然后后端自己签发一个业务 token 返回给前端。手机号获取则是另一条链路前端用button组件的open-typegetPhoneNumber拿到加密的encryptedData和iv后端用 session_key 解密。常见做法是后端把 openid 作为用户唯一标识手机号作为联系方式存进用户表。这里有个容易忽略的点session_key 会过期不能长期存着当解密密钥用每次解密前要确保 session_key 是新鲜的。我一般会在用户表里存 openid、unionid如果有、手机号、创建时间token 用 JWT 或者简单的 Redis 映射都行。// 小程序端登录并获取手机号 wx.login({ success: (res) { // res.code 传给后端换 openid wx.request({ url: https://your-api.com/auth/login, method: POST, data: { code: res.code }, success: (loginRes) { // 后端返回业务 token存起来 wx.setStorageSync(token, loginRes.data.token); } }); } }); // 手机号授权按钮 // button open-typegetPhoneNumber bindgetphonenumberonGetPhone// 后端用 code 换 openid再解密手机号 // 伪代码以 Node.js 为例 const axios require(axios); const crypto require(crypto); async function login(code) { const url https://api.weixin.qq.com/sns/jscode2session?appid${APPID}secret${SECRET}js_code${code}grant_typeauthorization_code; const { data } await axios.get(url); // data 里有 openid 和 session_key const token signJwt({ openid: data.openid }); // session_key 存 Redis设一个合理的过期时间 await redis.set(sk:${data.openid}, data.session_key, EX, 7200); return { token, openid: data.openid }; } function decryptPhone(encryptedData, iv, sessionKey) { const decipher crypto.createDecipheriv(aes-128-cbc, Buffer.from(sessionKey, base64), Buffer.from(iv, base64)); decipher.setAutoPadding(true); let decoded decipher.update(Buffer.from(encryptedData, base64)); decoded Buffer.concat([decoded, decipher.final()]); return JSON.parse(decoded.toString()); }参数说明APPID和SECRET在小程序后台「开发管理」里拿不要写死在前端。session_key的有效期官方没有明确承诺实践中按两小时缓存比较稳妥。解密失败最常见的原因是 session_key 对不上——用户可能在小程序里切换了账号或者后端缓存的 key 已经过期。2.2 号源时段表的设计与预约提交接口号源管理是这类系统的核心。我见过两种设计一种是把每个时段做成一条记录字段有日期、开始时间、结束时间、总号数、已约数另一种是拆成「号源池 预约记录」两张表号源池只管容量预约记录管占用。前者简单后者好扩展。源码包里常见的是前者够用。表结构大致是这样字段类型说明idbigint主键vaccine_idint疫苗种类datedate接种日期start_timetime时段开始end_timetime时段结束totalint总号数bookedint已预约数statustinyint0 正常 1 停诊预约提交接口要做的事校验用户是否重复预约、校验号源是否还有余量、扣减号源、写入预约记录。这四步必须在一个事务里否则并发下会出现超约。-- 扣减号源用条件更新防止超卖 UPDATE slot SET booked booked 1 WHERE id ? AND booked total AND status 0; -- 检查影响行数如果为 0 说明没抢到// 预约提交伪代码 async function submitAppointment(userId, slotId) { return await db.transaction(async (trx) { // 1. 查是否已约过同一疫苗同一日期 const exist await trx(appointment) .where({ user_id: userId, slot_id: slotId, status: 0 }).first(); if (exist) throw new Error(请勿重复预约); // 2. 条件扣减号源 const affected await trx(slot) .where(id, slotId) .andWhere(booked, , trx.raw(total)) .andWhere(status, 0) .update({ booked: trx.raw(booked 1) }); if (affected 0) throw new Error(该时段已约满); // 3. 写预约记录 await trx(appointment).insert({ user_id: userId, slot_id: slotId, status: 0, created_at: new Date() }); return { ok: true }; }); }逻辑说明条件更新booked total是防超卖的关键数据库行锁保证同一时刻只有一个请求能成功。参数上status字段用来做停诊控制停诊时直接拒绝新预约但不影响已有记录。事务隔离级别用默认的即可MySQL 的 InnoDB 在更新时会加行锁不需要额外加锁。3. 把模块拆开看疫苗管理、接种记录与消息通知3.1 疫苗种类与接种剂次的管理后台疫苗不是单一品类有新冠、流感、HPV、乙肝等每种疫苗的剂次规则不同。HPV 要打三针流感每年一针新冠有基础针和加强针。源码包里如果只做了「一种疫苗一个号源池」二次开发时大概率要改。我的做法是加一张vaccine_schedule表记录疫苗 ID、剂次序号、间隔天数。用户预约第一针后系统根据间隔天数自动算出第二针的可预约日期在接种记录页提示「您可在 X 月 X 日后预约第二针」。管理后台则提供疫苗的增删改查和号源批量生成——按日期范围、时段模板一次性生成一周或一个月的号源比手工一条条加效率高得多。// 批量生成号源伪代码 async function batchGenerateSlots(vaccineId, startDate, endDate, template) { // template 形如 [{start:08:00, end:08:30, total:20}, ...] const slots []; let d new Date(startDate); while (d new Date(endDate)) { for (const t of template) { slots.push({ vaccine_id: vaccineId, date: formatDate(d), start_time: t.start, end_time: t.end, total: t.total, booked: 0, status: 0 }); } d.setDate(d.getDate() 1); } await db(slot).insert(slots); }参数说明template是时段模板建议做成可配置的不同接种点的作息不一样。批量插入时注意单次插入条数MySQL 默认max_allowed_packet是 4MB几千条没问题上万的号源建议分批。3.2 接种记录查询与接种凭证的生成用户打完针之后要看记录接种点要出凭证。记录表里存用户 ID、疫苗 ID、剂次、接种日期、接种点、疫苗批号。查询接口按用户 ID 拉列表按时间倒序。凭证生成有两种做法一种是前端用 canvas 画一张图让用户保存另一种是后端生成 PDF。小程序里 canvas 方案更常见不依赖服务端渲染。// 小程序端用 canvas 绘制接种凭证 const ctx wx.createCanvasContext(certCanvas); ctx.setFillStyle(#ffffff); ctx.fillRect(0, 0, 300, 400); ctx.setFillStyle(#333333); ctx.setFontSize(18); ctx.fillText(疫苗接种凭证, 80, 40); ctx.setFontSize(14); ctx.fillText(姓名${user.name}, 20, 90); ctx.fillText(疫苗${record.vaccineName}, 20, 120); ctx.fillText(剂次第 ${record.dose} 针, 20, 150); ctx.fillText(日期${record.date}, 20, 180); ctx.draw(); // 再调 wx.canvasToTempFilePath 保存图片逻辑说明canvas 的坐标和字号要按实际设计稿调draw是异步的保存图片要放在draw的回调里。参数上凭证上的批号字段建议从接种记录里带出来不要前端写死。3.3 订阅消息在预约提醒中的落地微信小程序的订阅消息是预约提醒的主要手段。用户预约成功后前端调wx.requestSubscribeMessage请求订阅授权后端在接种前一天调微信的发送接口推提醒。注意两点一是订阅消息必须由用户主动触发授权不能静默订阅二是模板 ID 要在小程序后台申请字段顺序要和模板一致。// 后端发送订阅消息 async function sendRemind(openid, appointment) { const url https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token${token}; await axios.post(url, { touser: openid, template_id: 你的模板ID, page: pages/appointment/detail?id appointment.id, data: { thing1: { value: appointment.vaccineName }, time2: { value: appointment.date appointment.startTime }, thing3: { value: appointment.siteName } } }); }参数说明access_token要用中控服务统一管理不要每次发送都去换微信对获取频率有限制。data里的字段名要和模板里的占位符一一对应类型也要匹配thing类型限 20 个字符以内超了会报错。4. 避坑与排查源码跑不起来时先看这几处4.1 登录报 40029 或 invalid code现象前端调登录接口后端换 openid 时微信返回errcode: 40029提示 code 无效。原因通常是 code 被用了两次或者前端在wx.login的回调里又调了一次登录。解决确保一个 code 只换一次登录按钮加防抖后端换 openid 失败时把原始错误打日志不要吞掉。4.2 号源显示有余额但提交提示约满现象列表页显示某时段还剩 3 个号点进去提交却提示已约满。原因是列表页的余量是缓存或延迟数据提交时才是实时扣减。解决列表页的余量只做展示参考提交接口必须走条件更新如果对实时性要求高可以在列表页也查一次实时余量但不要在前端做扣减判断。4.3 手机号解密失败返回 -41003现象解密手机号时抛异常错误码-41003提示解密失败。原因基本是 session_key 不匹配——用户可能在小程序里清了缓存重新登录或者后端缓存的 session_key 过期了。解决解密前先确认 session_key 是当前登录态对应的建议在登录接口里把 session_key 和 token 一起返回给后端缓存解密时按 openid 取最新的。4.4 订阅消息发送成功但用户没收到现象后端调发送接口返回errcode: 0但用户说没收到提醒。原因有三种用户没有授权订阅、模板 ID 和实际申请的不一致、用户把小程序的通知关了。解决发送前检查用户是否有订阅记录模板 ID 从配置中心读不要写死发送失败的返回体要完整打日志errcode非 0 时按微信文档排查。4.5 批量生成号源时日期跨月出错现象按日期范围生成号源跨月时少生成或多生成一天。原因是setDate在月末会自动进位比如 1 月 31 日加一天变成 2 月 1 日但循环条件用的是字符串比较。解决日期统一用Date对象做加减格式化输出时再转字符串循环条件用时间戳比较。5. 进阶技巧用压测验证号源并发扣减是否真的可靠源码跑通只是第一步能不能扛住放号瞬间的并发才是这类系统的生死线。我一般会在本地用autocannon或wrk对预约提交接口做压测模拟 500 个并发请求抢 100 个号看最终booked是否等于total有没有超卖。# 用 autocannon 压测预约接口 npx autocannon -c 500 -d 10 -m POST \ -H Content-Type: application/json \ -H Authorization: Bearer token \ -b {slotId: 123} \ http://localhost:3000/api/appointment/submit压测完查数据库SELECT id, total, booked FROM slot WHERE id 123; -- booked 应该等于 total不能大于 SELECT COUNT(*) FROM appointment WHERE slot_id 123 AND status 0; -- 预约记录数应该等于 total如果booked大于total说明条件更新没生效检查 SQL 里booked total的条件是不是写成了booked total或者事务隔离级别是不是被改成了读未提交。如果预约记录数大于total说明事务没包住扣减和插入检查代码里两步是不是在同一个transaction里。另一个验证点是重复预约。用同一个用户 ID 并发提交两次看是否只有一条成功。这依赖appointment表上的唯一索引建议加UNIQUE KEY (user_id, slot_id, status)但status会变所以更稳妥的做法是在业务层用「查 插」加事务或者用 Redis 做短期的幂等键。// 用 Redis 做幂等同一用户同一时段 5 秒内只允许提交一次 const key appt:${userId}:${slotId}; const ok await redis.set(key, 1, NX, EX, 5); if (!ok) throw new Error(请勿重复提交);参数说明EX 5是 5 秒过期按实际接口耗时调整太短起不到防重作用太长会影响用户改选其他时段。这个方案不能替代数据库唯一约束只是减少无效请求打到数据库。我自己的习惯是任何预约类系统上线前必须跑一次并发压测并且把「超卖」和「重复预约」两个场景写成自动化测试用例每次改预约逻辑都跑一遍。这套源码包值不值得投入很大程度上取决于它的扣减逻辑是不是经得起压。如果源码里用的是「先查再更新」而不是条件更新那二次开发的第一件事就是把它改掉。希望帮到你。本文还有配套的精品资源点击获取