☰
微信小程序医院预约挂号系统源码解析:号源模型与并发预约设计
2026/10/28 14:34:58 网站建设 项目流程

简介:这份资源是面向计算机专业毕业设计场景的微信小程序医院预约挂号系统配套文档,适合正在准备Java方向毕设的本科生与高职学生参考。文档围绕微信小程序前端、Spring Boot后端与MySQL数据库的整体架构展开,涵盖用户管理、医生排班、在线预约挂号、微信消息提醒等核心模块,并讨论了HTTPS传输加密、缓存削峰与异常监控等实现思路,可帮助读者理解一个完整挂号系统的业务闭环与技术选型依据。压缩包内共1个docx文件,约883KB,内容以需求分析、系统设计、关键模块实现与总结等章节组织,便于按目录快速定位所需部分。目前已有382人学习下载,适合需要梳理毕设选题思路、撰写论文技术章节或对照搭建同类项目的读者参考借鉴。

1. 预约挂号小程序:为什么“号源”比“页面”难做一百倍

很多人第一次看医院预约挂号类小程序源码,注意力都落在页面上:科室列表、医生卡片、预约按钮,画出来就以为完事了。真正跑起来才发现,页面是最不值钱的部分,真正让人翻车的是号源——同一个医生、同一个上午、三十个号,被两百个人同时点“预约”,谁拿到、谁落空、数据库里那条记录怎么保证不重不漏。这才是这类系统的底层命题。

这份微信小程序医院预约挂号系统源码,把小程序端、后台管理端和 MySQL 数据层串成了一条完整链路,正好可以用来拆解“号源”这件事从表结构到并发写入的全过程。搞懂它,你再看任何预约类系统,都不会只盯着界面看。

2. 号源模型:一张排班表如何决定整个系统的上限

2.1 为什么不能把“剩余号数”直接塞进医生表

初学者最容易犯的错,是在医生表里加一个remain_count字段,预约一次减一。这个设计在单机、低并发、无退号的场景下能跑,但它把三个不同维度的信息压成了一列:医生是谁、哪天出诊、还剩几个号。一旦要支持“提前一周或一个月预约”,医生表就得按日期裂开,字段爆炸。

合格的实现思路是把“医生”和“排班”拆成两张表。医生表只存静态属性,排班表存“某医生某天某时段放多少号、已约多少号”。这份源码的数据库设计里,科室表、医生信息表、预约记录表是分开的,预约记录表才是号源的唯一事实来源,排班上的计数只是缓存出来的冗余。这样退号、改期、停诊都只需要动预约记录,再回写计数,不会出现“医生表说还有号、预约表已经满了”的黑匣子状态。

2.2 排班表的关键字段与约束

把排班抽象出来后,核心字段大致是这样一组:

CREATE TABLE doctor_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT '医生ID', dept_id BIGINT NOT NULL COMMENT '科室ID,冗余便于按科室筛选', work_date DATE NOT NULL COMMENT '出诊日期', time_slot TINYINT NOT NULL COMMENT '时段:1上午 2下午', total_count INT NOT NULL COMMENT '放号总数', booked_count INT NOT NULL DEFAULT 0 COMMENT '已预约数', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停诊', UNIQUE KEY uk_doctor_date_slot (doctor_id, work_date, time_slot) ) ENGINE=InnoDB;

uk_doctor_date_slot这个唯一索引是整个设计的锚点:它保证同一个医生同一天同一时段只有一条排班,后台管理员重复录入会直接被数据库挡回去,而不是靠前端校验。booked_count是冗余字段,存在的意义是列表页不用对预约表做 count 聚合——科室页要一次展示几十个医生的号源状态,每次都 count 一遍预约表,数据库会被拖垮。冗余的代价是要保证它和预约记录一致,这就引出下一节的并发问题。

2.3 预约记录表:号源的唯一事实来源

预约记录表承担三件事:谁约的、约的哪个排班、当前是什么状态。状态机通常只有几个值:待就诊、已取消、已完成、已爽约。真正决定“这个号还在不在”的,是“待就诊”状态的记录数是否小于total_count。

CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL COMMENT '患者ID', schedule_id BIGINT NOT NULL COMMENT '排班ID', doctor_id BIGINT NOT NULL COMMENT '医生ID,冗余便于后台按医生查', appoint_date DATE NOT NULL COMMENT '就诊日期,冗余便于按日统计', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待就诊 1已取消 2已完成 3爽约', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_patient_schedule (patient_id, schedule_id, status) ) ENGINE=InnoDB;

这里有个细节值得说:唯一索引把status也带上了。原因是患者取消后可以重新约同一个排班,如果只对(patient_id, schedule_id)建唯一索引,取消过的记录会挡住重新预约。带上status后,同一患者在同一排班下最多只能有一条“待就诊”记录,取消记录可以有多条,既防了重复预约,又允许退号重约。这种“用索引表达业务规则”的做法,比在代码里写一堆 if 判断可靠得多。

3. 并发预约:两个人同时点“确认”,数据库到底发生了什么

3.1 先查后改的经典翻车现场

最直觉的写法是:查排班,判断booked_count < total_count,然后插入预约记录、更新计数。单线程下没问题,两个请求同时进来就出事——两个事务都读到booked_count = 29、total_count = 30,都判断“还有号”,都插入成功,最后booked_count变成 31,超卖。

这不是玄学,是隔离级别下的正常现象。MySQL 默认的可重复读(RR)下,普通SELECT是快照读,不加锁,两个事务看到的是同一份旧快照。要堵住这个口子,要么让判断和写入变成原子操作,要么在读取时就加锁。

3.2 方案一:条件更新,把判断压进 SQL

最轻量的做法是不在应用层判断,把条件写进UPDATE的WHERE里:

UPDATE doctor_schedule SET booked_count = booked_count + 1 WHERE id = #{scheduleId} AND status = 1 AND booked_count < total_count;

这条语句执行后返回受影响行数。返回 1 表示抢号成功,返回 0 表示号已满或已停诊,应用层据此决定是继续插入预约记录还是提示“号源已满”。UPDATE在 InnoDB 里会对命中行加排他锁,两个并发事务会被串行化,第二个事务重新读到的booked_count已经是加过的值,条件自然不成立。

这个方案的好处是简单、无额外依赖,缺点是“先占计数、再插记录”不是同一个事务时,中间失败会导致计数虚高。所以必须把两条语句包在一个事务里,插入失败就回滚计数。常见的实现思路是:开事务 → 条件更新计数 → 插入预约记录 → 提交;任何一步异常都回滚。

3.3 方案二:行锁 + 事务,适合需要复杂校验的场景

如果预约前还要校验患者是否被拉黑、是否重复预约、是否超出个人预约上限,单条UPDATE就不够了,需要在事务里先SELECT ... FOR UPDATE锁住排班行:

START TRANSACTION; SELECT total_count, booked_count, status FROM doctor_schedule WHERE id = #{scheduleId} FOR UPDATE; -- 应用层判断号源、校验患者资格 INSERT INTO appointment (patient_id, schedule_id, doctor_id, appoint_date, status) VALUES (#{patientId}, #{scheduleId}, #{doctorId}, #{appointDate}, 0); UPDATE doctor_schedule SET booked_count = booked_count + 1 WHERE id = #{scheduleId}; COMMIT;

FOR UPDATE拿到的是当前读加排他锁,其他事务在这一行上会阻塞,直到本事务提交。代价是锁持有时间变长,如果事务里还夹着远程调用或慢查询,排队会很明显。所以这个方案适合号源粒度细、单排班竞争不极端的场景;如果热门专家号一秒几百个请求,锁竞争会成为瓶颈,得往上加缓存预扣或消息队列削峰。

3.4 退号为什么比预约更容易出 bug

退号要做两件事:把预约记录状态改成“已取消”,把booked_count减一。看起来对称,坑在于重复退号和并发退号。患者连点两次“取消”,两个请求都读到“待就诊”,都去减计数,号源就凭空多出来一个。

解决办法还是靠状态机加条件更新:UPDATE appointment SET status = 1 WHERE id = ? AND status = 0,受影响行数为 1 才继续减计数,为 0 说明已经被取消过,直接返回。计数更新同样用booked_count = booked_count - 1 WHERE booked_count > 0兜底,防止减成负数。这套“状态条件更新 + 计数边界保护”的组合,是退号场景的血泪经验,少一个都可能在对账时发现号源总数对不上。

4. 小程序端到数据库:一次预约请求的完整链路

4.1 请求是怎么穿过三层的

小程序端点“确认预约”,走的是wx.request发起的 HTTPS 请求,落到后端接口,后端再操作 MySQL。这条链路上每一层都有各自的职责边界,混淆了就会出问题。

小程序端只负责收集参数(排班 ID、患者标识)和展示结果,不做任何号源判断——前端判断只能防误触,防不了并发。后端接口负责鉴权、参数校验、事务编排,是业务规则的唯一执行者。MySQL 负责最终的数据一致性,唯一索引和行锁是最后一道防线。常见的实现思路是后端接口保持无状态,患者身份从登录态里取,不信任前端传的patient_id,否则改个参数就能替别人挂号。

4.2 接口层的幂等设计

移动网络下,用户点一次按钮可能发出两个请求,或者超时重发。如果接口不幂等,就会产生两条预约记录。前面预约表上的uk_patient_schedule唯一索引在这里发挥作用:第二个请求插入时触发唯一键冲突,后端捕获后返回“您已预约该时段”,而不是抛 500。

更稳妥的做法是让小程序端在请求头带一个客户端生成的请求号,后端用 Redis 做短期的请求号去重。但这份源码的规模下,数据库唯一索引已经够用,加 Redis 属于过度设计。判断标准很简单:如果重复请求的后果只是多一条脏数据、且能被唯一索引挡住,就不必引入额外组件;如果重复请求会触发扣款、发短信这类副作用,才需要显式幂等表。

4.3 列表页的查询怎么不拖垮数据库

科室页要展示“某科室下所有医生未来一周的号源情况”,这是典型的 N+1 查询重灾区:先查医生列表,再对每个医生查排班。十个医生就是十一次查询,科室一多,页面加载肉眼可见地慢。

合理的做法是一次性把排班按医生 ID 批量查出来,在应用层按doctor_id分组拼装。SQL 大致是SELECT doctor_id, work_date, time_slot, total_count, booked_count FROM doctor_schedule WHERE doctor_id IN (...) AND work_date BETWEEN ? AND ? AND status = 1,配合(doctor_id, work_date)上的索引,一次查询解决。这也是为什么排班表要把dept_id冗余进去——按科室筛排班时可以直接走科室维度,不必回表关联医生表。

5. 避坑与排查:这类系统上线后最容易踩的五个坑

5.1 号源显示“有号”,点进去却约不上

现象:列表页显示某医生还有 3 个号,点进详情点预约,提示“号源已满”。

原因:列表页读的是booked_count冗余字段,详情页或预约接口读的是预约记录实时 count,两者在并发下短暂不一致;或者列表页有缓存,缓存未及时失效。

解决:统一以排班表的booked_count为准,预约接口的条件更新也基于这一列,保证读写同源;列表页缓存设置较短的过期时间,预约成功后主动失效对应医生的缓存键。

5.2 取消预约后号源没有回来

现象:患者取消成功,页面也提示成功,但该时段仍显示满号。

原因:退号逻辑只改了预约记录状态,忘了回写booked_count;或者回写时条件写错,WHERE status = 0在更新预约记录之后才执行,此时状态已是 1,计数更新影响行数为 0。

解决:把“改状态”和“减计数”放在同一事务,先条件更新预约状态并检查受影响行数,为 1 才执行减计数;上线前用一条脚本对账:SELECT schedule_id, COUNT(*) FROM appointment WHERE status = 0 GROUP BY schedule_id与排班的booked_count逐条比对,差异即为 bug。

5.3 同一患者出现两条待就诊记录

现象:后台按患者查预约,同一排班下有两条“待就诊”。

原因:唯一索引没建,或者建了但没带status导致取消后重约失败,开发者索性去掉索引;也可能是历史数据迁移时绕过了约束。

解决:补上(patient_id, schedule_id, status)唯一索引,迁移前先清洗重复数据;接口层捕获DuplicateKeyException转成友好提示,而不是让异常冒到前端变成“系统错误”。

5.4 后台停诊后,已预约的患者无人通知

现象:管理员把某天排班设为停诊,患者毫不知情,到院才发现。

原因:停诊只改了排班status,没有联动处理该排班下“待就诊”的预约记录。

解决:停诊操作做成一个事务:更新排班状态 → 查出该排班所有待就诊预约 → 批量置为“已取消”并记录原因 → 触发通知。通知渠道可以是小程序订阅消息,但要注意订阅消息需要用户授权,不能假设一定能送达,后台列表里要能看到“待通知/已通知”状态,方便人工兜底。

5.5 高峰期接口超时,事务长时间不提交

现象:放号瞬间接口大量超时,数据库连接池被打满。

原因:预约事务里夹了非数据库操作,比如调用短信接口、写日志到远程服务,导致行锁持有时间被拉长,后续请求全部排队。

解决:事务里只做数据库操作,通知类副作用放到事务提交后异步执行;连接池大小和数据库最大连接数对齐,避免应用层排队掩盖数据库层瓶颈;对热门排班可以考虑在入口层做限流,把无效请求挡在事务之外。

6. 设计取舍:为什么“够用”比“先进”更难判断

这套源码的技术栈是微信小程序 + 后端服务 + MySQL,没有引入 Redis 预扣、没有消息队列、没有分库分表。放在高并发架构的语境里,它显得朴素;但放在“一个医院、几十个科室、几百个医生、日预约量几千”的真实规模里,这套设计恰好卡在够用和不过度的分界线上。

值得琢磨的是几个取舍点。第一,号源计数用冗余字段而不是实时聚合,是用一致性风险换查询性能,代价是必须靠事务和条件更新守住一致性,这个交换在中小规模下划算,因为读远多于写。第二,并发控制选条件更新而不是分布式锁,是因为单库单表下数据库行锁已经足够,引入分布式锁反而多了一个可能失效的组件。第三,唯一索引承担业务约束,是把规则下沉到数据层,应用层可以写得薄,但要求开发者理解索引语义,否则一个status字段的取舍就能让退号重约功能瘫痪。

我一般会这么理解:预约系统的复杂度不在代码量,而在“状态”和“并发”这两个词交叉的地方。状态机画清楚了,并发路径想明白了,剩下的都是体力活。反过来,表结构拍脑袋定,后面每加一个功能都在还债。

从那以后我每次看这类预约系统,都强制先做一件事:把排班表和预约表的索引、状态字段、计数更新语句单独拎出来,在纸上推演两个并发请求和一次退号的执行顺序,推不通就不看页面。这个习惯帮我省掉了太多上线后的对账夜。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询