微信小程序预约挂号系统实战:高并发号源扣减与防超卖设计

发布时间:2026/10/11 10:15:14
微信小程序预约挂号系统实战:高并发号源扣减与防超卖设计
简介这份资源是面向计算机专业毕业设计学生与Java Web初学者的一套完整论文资料围绕基于微信小程序的预约挂号系统展开帮助读者理解从需求分析到系统落地的全过程。压缩包内共1个doc文件约2.13MB内容涵盖任务书、开题报告与论文正文结构完整可直接作为毕设写作与答辩参考。系统采用Java SSM框架搭建后台管理MySQL作为本地数据库前端借助微信开发者工具实现小程序端划分管理员、医生、用户三种角色管理员负责用户、医生、科室、排班、预约、取消预约、调班申请及系统管理医生可注册登录并处理预约与调班用户则能查看医生信息、通知公告并完成预约操作。目前已有404人学习下载适合需要选题参考、功能模块梳理与数据库设计思路的读者可据此快速把握预约挂号类系统的整体架构与实现要点。1. 从一份真实需求说起预约挂号系统为什么总在“最后一公里”翻车做过医疗信息化的人都有一个共识挂号系统的技术难点从来不在“挂号”这个动作本身而在号源分配、并发抢号、身份核验和退号回收这四件事同时发生时系统还能不能稳住。基于微信小程序的预约挂号系统本质上是一套“轻前端 重后端 强约束”的业务系统小程序负责降低患者使用门槛后端负责把号源这张“有限库存表”管明白。它适合两类人一类是正在做毕业设计或课程设计的学生需要一套能跑通、能讲清楚、能写进论文的完整方案另一类是中小型医疗机构的信息化从业者想用最低成本搭一套可用的预约流程。这篇笔记不讲空泛的架构图只讲我实际落地时怎么选型、怎么建表、怎么防超卖、怎么排错。2. 技术选型与整体架构为什么是微信小程序加自建后端2.1 小程序端为什么不用纯云开发很多人第一反应是用微信云开发省去服务器和运维。我一开始也这么想但真做到号源并发扣减这一步就发现云开发的数据库事务能力在跨集合操作时限制较多而挂号的核心恰恰是“查余号 扣余号 写订单”必须在一个原子操作里完成。常见做法是自建后端小程序只做展示和请求转发。技术栈上后端用 Spring Boot 或 Node.js 都可以数据库用 MySQL 加 Redis 的组合最稳MySQL 存号源和订单的持久化数据Redis 做号源库存的原子扣减和分布式锁。小程序端用原生 WXML 或者 uni-app 都行原生对微信登录态的处理更直接uni-app 适合后续要扩展到其他端的情况。2.2 整体架构分层系统分四层小程序展示层、接口网关层、业务服务层、数据存储层。小程序通过 wx.login 拿到 code传给后端换 openid后端用 openid 作为患者唯一标识。接口网关层做鉴权和限流业务服务层拆成号源服务、订单服务、排班服务三个模块。数据存储层里MySQL 存医生、科室、排班、订单四张主表Redis 存号源余量和短期会话。这个分层的好处是号源扣减这种高并发操作可以单独优化不影响排班查询这类读多写少的接口。2.3 数据库表设计的关键字段排班表是核心它决定了“哪个医生在哪个时间段有多少个号”。我一般会这样建-- 排班表一个医生一天可以有多条排班记录 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, department_id BIGINT NOT NULL COMMENT 科室ID, work_date DATE NOT NULL COMMENT 出诊日期, time_slot TINYINT NOT NULL COMMENT 时段1上午 2下午, total_quota INT NOT NULL DEFAULT 0 COMMENT 总号源数, remaining_quota INT NOT NULL DEFAULT 0 COMMENT 剩余号源数, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_doctor_date_slot (doctor_id, work_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数要说明。remaining_quota是扣减的目标字段version是乐观锁版本号防止并发更新时覆盖。uk_doctor_date_slot这个唯一索引保证同一个医生同一天同一时段只有一条排班记录避免重复排班导致号源翻倍。订单表里要存schedule_id和patient_openid并且对(schedule_id, patient_openid)建唯一索引防止同一个人对同一排班重复下单。2.4 号源扣减的两种实现路径第一种是数据库乐观锁适合并发量不大的场景UPDATE schedule SET remaining_quota remaining_quota - 1, version version 1 WHERE id ? AND remaining_quota 0 AND version ?;执行后检查影响行数如果是 0 说明被抢完了或者版本冲突需要重试或返回失败。第二种是 Redis 预扣减适合号源紧张、瞬时并发高的场景。思路是排班创建时就把余号写入 Redis扣减用 Lua 脚本保证原子性-- KEYS[1]: 号源key ARGV[1]: 扣减数量 local remain tonumber(redis.call(GET, KEYS[1])) if remain nil or remain tonumber(ARGV[1]) then return -1 end redis.call(DECRBY, KEYS[1], ARGV[1]) return remain - tonumber(ARGV[1])返回 -1 表示余号不足返回其他值表示扣减成功后的余量。Redis 扣减成功后再异步写 MySQL 订单用消息队列保证最终一致。我一般会两种都用Redis 挡第一层流量MySQL 乐观锁做兜底对账。3. 从零跑通核心流程登录、选号、下单、退号3.1 微信登录态换取与患者身份绑定小程序端调用 wx.login 拿到临时 code传给后端// 小程序端 wx.login({ success: (res) { wx.request({ url: https://your-domain.com/api/auth/login, method: POST, data: { code: res.code }, success: (resp) { // resp.data.token 存入 storage后续请求带上 wx.setStorageSync(token, resp.data.token); } }); } });后端收到 code 后用 appid 和 secret 调用微信接口换 openid 和 session_key。这里注意session_key 不要下发给小程序端只存在后端用自定义 token 做会话标识。openid 作为患者唯一标识写入用户表。如果患者是首次登录同时创建用户记录如果不是直接返回 token。这个流程里最容易翻车的地方是 code 只能用一次如果前端重复提交同一个 code第二次会失败所以前端要做防抖。3.2 号源查询与排班展示患者选科室和日期后前端请求排班列表// 请求排班列表 wx.request({ url: https://your-domain.com/api/schedule/list, data: { departmentId: 1, date: 2025-06-01 }, header: { Authorization: wx.getStorageSync(token) }, success: (res) { // res.data 是排班数组包含医生名、时段、剩余号源 } });后端查询时优先从 Redis 读余量Redis 没有再从 MySQL 读并回写 Redis。这里有个细节排班列表的余量展示允许有轻微延迟但下单扣减必须准确。所以查询接口可以走缓存扣减接口必须走原子操作。我一般会在排班查询接口里加一个remaining_quota字段直接从 Redis 读如果 Redis 挂了就降级读 MySQL。3.3 下单接口的完整逻辑与防重下单是核心接口逻辑顺序是校验 token、校验排班是否存在、校验是否重复下单、扣减号源、创建订单、返回结果。防重这块除了数据库唯一索引我还会在 Redis 里用setnx做一个短期的防重锁# 伪代码下单防重 lock_key forder_lock:{schedule_id}:{openid} if not redis.setnx(lock_key, 1): return {code: 429, msg: 请勿重复提交} redis.expire(lock_key, 5) # 5秒内不允许重复提交 try: # 执行扣减和创建订单 ... finally: redis.delete(lock_key)这个锁的过期时间不能太长5 秒足够覆盖一次请求太长会影响患者正常重试。扣减成功后创建订单订单状态初始为“待就诊”。如果扣减成功但订单创建失败要有补偿机制比如把号源加回去或者记录异常日志人工处理。3.4 退号与号源回收退号不是简单地把订单状态改成“已取消”还要把号源加回去。这里有个坑如果退号时直接把remaining_quota加一可能会超过total_quota。所以退号逻辑要判断当前余量是否小于总号源UPDATE schedule SET remaining_quota remaining_quota 1 WHERE id ? AND remaining_quota total_quota;同时更新订单状态为“已取消”并记录退号时间。如果患者是就诊当天退号很多医院规定不允许退这个规则要在业务层判断。退号后 Redis 里的余量也要同步加回去保证缓存和数据库一致。4. 避坑与排查那些让我熬夜的线上问题4.1 号源超卖现象是余量显示有号但下单失败现象患者看到余量还有 3 个点进去下单却提示“号源不足”。原因通常是 Redis 和 MySQL 数据不一致或者扣减时没有加原子性保证。解决方法是扣减必须用 Lua 脚本或数据库行锁不能先查再减。另外排班创建时要确保 Redis 余量和 MySQL 一致我一般会在排班创建后手动同步一次并加一个定时对账任务每 5 分钟比对一次 Redis 和 MySQL 的余量不一致就以 MySQL 为准回写 Redis。4.2 重复下单同一患者对同一排班生成两条订单现象患者快速点击两次提交生成了两条订单。原因是前端没有防抖后端没有防重。解决方法是前端按钮点击后置灰后端用 Redis setnx 加唯一索引双保险。唯一索引是最后一道防线即使 Redis 锁失效数据库也会拒绝第二条插入。注意唯一索引的字段组合要是(schedule_id, patient_openid, status)其中 status 只对“待就诊”状态生效已取消的订单不占用唯一性。4.3 微信登录态过期token 失效导致接口 401现象患者用了一段时间后所有接口返回 401。原因是自定义 token 过期了但前端没有刷新机制。解决方法是在 token 里放过期时间前端在请求拦截器里判断快过期时重新调用 wx.login 换新 token。另外后端返回 401 时前端要能捕获并自动跳转登录而不是直接报错。我一般会把 token 有效期设成 7 天同时提供一个 refresh 接口用旧 token 换新 token。4.4 排班时间跨天日期边界导致号源错乱现象晚上 11 点挂第二天的号结果扣的是当天的号源。原因是后端用服务器时间判断“今天”但服务器时区可能不是东八区。解决方法是所有时间判断统一用东八区并且在排班表里存的是work_date日期字段不是时间戳。查询时用work_date ?精确匹配不要用时间范围去查。另外小程序端传日期时要用YYYY-MM-DD格式不要传时间戳避免时区转换问题。4.5 Redis 宕机导致号源扣减全部失败现象Redis 挂了之后所有下单接口报错。原因是扣减逻辑强依赖 Redis没有降级方案。解决方法是加降级开关Redis 不可用时自动切到 MySQL 乐观锁扣减。虽然性能会下降但至少能保证业务不中断。我一般会在扣减接口里加 try-catch捕获 Redis 连接异常后走数据库扣减并打日志告警。同时Redis 要做持久化重启后能恢复余量数据。5. 论文写作与系统验证怎么把工程细节变成可答辩的内容5.1 论文里该放哪些图和表论文不是代码仓库不需要贴全部源码。我一般会放这几张图系统架构图四层分层、号源扣减流程图Redis 加 MySQL 双路径、数据库 ER 图医生、排班、订单、患者四张表的关系。表格方面放一张接口清单表列出接口名、方法、参数、返回值再放一张测试用例表列出并发场景、预期结果、实际结果。这些图表能让评委快速理解系统边界比大段文字有效。5.2 性能测试怎么做才有说服力不要只写“系统响应快”要有数据。我一般用 JMeter 或 wrk 做压测模拟 100 个并发用户同时抢 50 个号。测试指标包括平均响应时间、TPS、错误率、号源是否超卖。测试结果里Redis 加 MySQL 双路径的 TPS 通常比纯 MySQL 高 3 到 5 倍。把这些数据做成折线图或柱状图放进论文比空谈“高并发”有说服力。注意压测环境要说明机器配置和网络条件否则数据没有参考意义。5.3 答辩时最容易被问到的三个问题第一个是“你怎么防止超卖”回答要点是 Redis Lua 原子扣减加数据库唯一索引兜底。第二个是“微信登录态怎么维护”回答要点是 code 换 openid自定义 token 做会话token 过期用 refresh 接口续期。第三个是“退号后号源怎么回收”回答要点是判断余量小于总号源才加回同时同步 Redis 和 MySQL。这三个问题答清楚了基本就能过。我见过有人被问“如果 Redis 和 MySQL 数据不一致怎么办”这时候要答对账任务和降级方案不能只说“不会不一致”。5.4 一个我常用的验证技巧用对账脚本兜底不管并发多高最终都要以 MySQL 为准。我一般会写一个对账脚本每天凌晨跑一次比对 Redis 余量和 MySQL 余量不一致就告警并修复。脚本逻辑很简单# 对账脚本伪代码 schedules mysql.query(SELECT id, remaining_quota FROM schedule WHERE work_date CURDATE()) for s in schedules: redis_remain redis.get(fquota:{s[id]}) if redis_remain is None or int(redis_remain) ! s[remaining_quota]: redis.set(fquota:{s[id]}, s[remaining_quota]) log.warning(f对账修复: schedule_id{s[id]}, mysql{s[remaining_quota]}, redis{redis_remain})这个脚本不复杂但能兜住大部分缓存不一致的问题。我自己的习惯是任何用缓存扛并发的系统都要有一个“以数据库为准”的对账任务不然出了问题连后悔药都没有。希望帮到你。本文还有配套的精品资源点击获取