尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
医院预约挂号小程序源码解析:号源并发扣减与订单状态回滚实战
简介这份资源是面向高校计算机专业毕业设计场景的微信小程序医院预约挂号系统配套源码与数据库文档适合正在准备Java方向毕设、需要完整项目参考的本科生及自学者。资源以docx文档形式交付压缩包内共1个文件约883KB内容围绕系统关键技术点展开涵盖微信小程序前端、Java后端、Spring Boot与SSM框架选型以及MySQL数据库设计等核心模块。文档从项目背景与意义切入梳理了传统窗口挂号效率低、专家号资源紧张等痛点并给出线上预约挂号的解决思路随后依次解析用户管理、医生管理、预约管理、通知提醒等功能模块的设计与实现同时涉及HTTPS数据传输、敏感信息加密、缓存优化与高并发处理等常见技术难点的应对方案。目前已有381人学习可为毕设选题、技术选型与论文撰写提供较完整的参考框架。1. 从一份医院预约挂号小程序源码说起它到底能跑通什么很多做医疗信息化的开发者拿到一份“医院预约挂号小程序源码数据库”时第一反应是打开压缩包看目录结构然后试图直接导入运行。但真正落地过的人都知道这类源码的价值不在于“能不能跑”而在于它把挂号业务里最麻烦的几件事——号源池的并发扣减、排班与号别的关系、订单状态机、取消预约后的号源回滚——用一套可读的代码固定下来了。你拿到的不只是一堆页面而是一个已经替你踩过坑的业务骨架。这份源码通常包含小程序端科室列表、医生详情、排班日历、确认下单、我的预约和服务端排班管理、号源生成、订单接口、支付回调数据库里至少会有科室表、医生表、排班表、号源表、订单表、用户表这几张核心表。它适合两类人一类是想快速搭一个预约类小程序原型的产品或全栈开发者另一类是正在做医疗类项目、需要参考号源并发控制写法的后端工程师。接下来我会按“先看懂数据模型再跑通最小链路最后处理并发和状态回滚”的顺序把这份源码里真正值得抄的部分拆开讲。2. 先看懂数据库号源、排班、订单三张表怎么串2.1 号源表不是排班表的附属而是独立库存很多新手会把“排班”和“号源”混成一张表结果一到取消预约就翻车。常见做法是拆成两张表doctor_schedule记录某医生某天上午/下午在哪个科室出诊、总号数、挂号费schedule_source记录每一条具体号源比如“第1号、第2号……第N号”每个号源有独立状态可约、已约、锁定、作废。这样做的原因是取消预约时只需要把对应号源状态改回“可约”而不是去改排班表的总数避免并发下计数错乱。-- 排班表定义医生某天某时段的出诊计划 CREATE TABLE doctor_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, dept_id BIGINT NOT NULL, work_date DATE NOT NULL, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, total_count INT NOT NULL DEFAULT 0, fee DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停诊, UNIQUE KEY uk_doctor_date_slot (doctor_id, work_date, time_slot) ); -- 号源表每一条记录就是一个可挂的号 CREATE TABLE schedule_source ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL, source_no INT NOT NULL COMMENT 第几号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可约 1已约 2锁定 3作废, order_id BIGINT DEFAULT NULL, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, UNIQUE KEY uk_schedule_no (schedule_id, source_no), KEY idx_schedule_status (schedule_id, status) );上面这段建表语句里version字段是后面做乐观锁的关键uk_schedule_no保证同一个排班下不会出现重复号。idx_schedule_status是为了查“某排班下还有多少可约号”时走索引。注意status用 0/1/2/3 而不是字符串是为了减少存储和比较开销。2.2 订单表要能追溯号源而不是只存医生和时间订单表如果只存doctor_id、work_date取消时你根本不知道要回滚哪个号。正确做法是订单表里存schedule_id和source_id并且把号源状态变更和订单状态变更放在同一个事务里。CREATE TABLE appointment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, source_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已预约 2已取消 3已完成 4已过期, pay_amount DECIMAL(10,2) NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status), KEY idx_source (source_id) );uk_order_no防止重复下单idx_user_status用于“我的预约”列表查询idx_source用于根据号源反查订单。这里status的状态机是待支付→已预约→已完成或者待支付→已过期已预约→已取消。状态流转必须用代码控制不能靠数据库默认值。2.3 用一条 SQL 查“某医生某天还有多少号”这是小程序端排班日历最常用的查询。不要用SELECT COUNT(*)再减已约数直接按状态统计。SELECT s.id AS schedule_id, s.work_date, s.time_slot, s.fee, SUM(CASE WHEN src.status 0 THEN 1 ELSE 0 END) AS available_count, SUM(CASE WHEN src.status 1 THEN 1 ELSE 0 END) AS booked_count FROM doctor_schedule s LEFT JOIN schedule_source src ON src.schedule_id s.id WHERE s.doctor_id ? AND s.work_date BETWEEN ? AND ? AND s.status 1 GROUP BY s.id, s.work_date, s.time_slot, s.fee ORDER BY s.work_date, s.time_slot;这条 SQL 的LEFT JOIN保证即使某个排班还没生成号源也能查出来available_count为 0。CASE WHEN比多次子查询快因为只扫一遍schedule_source。参数?分别是医生 ID、开始日期、结束日期。如果查出来available_count为 0小程序端就置灰该时段。3. 跑通最小预约链路从选号到下单的四个接口3.1 号源锁定接口先锁再付避免超卖用户点击“确认预约”时不能直接改号源状态为“已约”而是先改成“锁定”等支付成功再改成“已约”。锁定要加过期时间否则用户不支付会一直占号。常见做法是在schedule_source加lock_time字段或者用 Redis 存锁。# 伪代码号源锁定使用乐观锁防止并发 def lock_source(schedule_id, source_id, user_id): # 1. 查询号源当前状态和版本号 row db.query( SELECT id, status, version FROM schedule_source WHERE id%s AND schedule_id%s, (source_id, schedule_id) ) if not row or row.status ! 0: return {code: 4001, msg: 号源不可约} # 2. 乐观锁更新只有版本号匹配且状态仍为可约才更新 affected db.execute( UPDATE schedule_source SET status2, versionversion1, lock_timeNOW() WHERE id%s AND version%s AND status0, (source_id, row.version) ) if affected 0: return {code: 4002, msg: 手慢了请重新选择} # 3. 创建待支付订单 order_no gen_order_no() db.execute( INSERT INTO appointment_order (order_no, user_id, schedule_id, source_id, status) VALUES (%s, %s, %s, %s, 0), (order_no, user_id, schedule_id, source_id) ) return {code: 0, order_no: order_no}这段代码的关键在第二步UPDATE ... WHERE version%s AND status0。如果两个请求同时进来只有一个能更新成功另一个affected为 0直接返回“手慢了”。这比先SELECT再UPDATE安全得多。lock_time用于后续定时任务释放超时未支付的号源。3.2 支付回调接口把锁定号源变成已约支付回调必须做幂等因为支付平台可能重复通知。用order_no查订单如果已经是“已预约”就直接返回成功。def pay_callback(order_no, trade_no): order db.query(SELECT id, source_id, status FROM appointment_order WHERE order_no%s, (order_no,)) if not order: return {code: 4003, msg: 订单不存在} if order.status 1: return {code: 0, msg: 已处理} # 幂等 # 事务更新订单状态 更新号源状态 with db.transaction(): db.execute( UPDATE appointment_order SET status1, update_timeNOW() WHERE id%s AND status0, (order.id,) ) db.execute( UPDATE schedule_source SET status1, order_id%s WHERE id%s AND status2, (order.id, order.source_id) ) return {code: 0, msg: ok}这里UPDATE appointment_order ... WHERE status0保证只有待支付订单能被改成已预约。号源从status2锁定改成status1已约并回填order_id。两个更新在同一个事务里要么都成功要么都回滚。3.3 取消预约接口号源回滚与状态机校验取消预约不是简单把订单改成“已取消”还要把号源改回“可约”并且要判断当前订单状态是否允许取消。def cancel_order(order_no, user_id): order db.query( SELECT id, source_id, status FROM appointment_order WHERE order_no%s AND user_id%s, (order_no, user_id) ) if not order: return {code: 4004, msg: 订单不存在} if order.status not in (0, 1): return {code: 4005, msg: 当前状态不可取消} with db.transaction(): db.execute( UPDATE appointment_order SET status2, update_timeNOW() WHERE id%s AND status IN (0,1), (order.id,) ) db.execute( UPDATE schedule_source SET status0, order_idNULL, versionversion1 WHERE id%s, (order.source_id,) ) return {code: 0, msg: 取消成功}注意号源回滚时versionversion1这样如果有其他请求正在锁定这个号乐观锁会失败避免刚回滚就被错误锁定。status IN (0,1)保证只有待支付或已预约的订单能取消已完成或已过期的不能取消。3.4 超时未支付释放定时任务怎么写锁定号源后如果用户 15 分钟不支付需要自动释放。常见做法是用定时任务扫appointment_order里status0且create_time超过 15 分钟的订单逐个取消。-- 查出超时未支付订单 SELECT id, source_id FROM appointment_order WHERE status 0 AND create_time DATE_SUB(NOW(), INTERVAL 15 MINUTE) LIMIT 100;然后对每条订单执行和取消预约相同的逻辑订单改“已过期”号源改回“可约”。注意要加LIMIT避免一次锁太多行。定时任务频率建议 1 分钟一次用分布式锁保证只有一个实例在跑。4. 并发扣减号源乐观锁、悲观锁和 Redis 怎么选4.1 乐观锁适合号源竞争不激烈的场景上面用的version字段就是乐观锁。它的优点是实现简单、不依赖额外组件缺点是冲突多时大量请求失败重试。医院挂号通常一个医生一天几十个号并发不会像秒杀那么高乐观锁足够。但如果某个热门专家号放号瞬间有几千人抢乐观锁会导致大量“手慢了”用户体验差。4.2 悲观锁SELECT ... FOR UPDATE的边界如果一定要用数据库悲观锁可以这样写START TRANSACTION; SELECT id, status FROM schedule_source WHERE schedule_id ? AND status 0 ORDER BY source_no LIMIT 1 FOR UPDATE; -- 然后更新这条记录为锁定 UPDATE schedule_source SET status 2 WHERE id ?; COMMIT;FOR UPDATE会锁住查到的行其他事务阻塞。问题是如果WHERE条件没有走索引会锁表。所以schedule_id和status必须有联合索引。另外ORDER BY source_no LIMIT 1要配合索引否则排序也会锁更多行。这种写法适合号源数量少、并发集中的场景但不要在高并发下用。4.3 Redis 预扣减把库存放到内存里如果放号瞬间并发很高常见做法是提前把号源数量加载到 Redis用DECR原子操作扣减。扣减成功再异步落库。但要注意 Redis 和数据库的一致性如果 Redis 扣了但数据库写失败需要补偿。一般用 Redis 做第一层过滤数据库做最终一致。# Redis 预扣减示例 def try_lock_source_redis(schedule_id, source_id): key fsource:stock:{schedule_id} # 先判断该号源是否在可约集合里 if not redis.sismember(fsource:available:{schedule_id}, source_id): return False # 原子移除移除成功表示抢到 removed redis.srem(fsource:available:{schedule_id}, source_id) if removed 1: redis.hset(fsource:lock:{source_id}, user_id, user_id) redis.expire(fsource:lock:{source_id}, 900) return True return Falsesource:available:{schedule_id}是一个 Set存所有可约号源 ID。SREM是原子操作返回 1 表示抢到。抢到后写一个带过期时间的锁 key。后续支付成功再删掉锁 key 并更新数据库。如果支付失败或超时定时任务把号源 ID 重新SADD回集合。4.4 三种方案对比与选型建议方案实现复杂度并发能力一致性适用场景乐观锁低中强号源少、并发一般悲观锁中低强号源少、必须强一致Redis 预扣高高最终一致放号瞬间高并发我一般会先用乐观锁跑通业务等确实出现性能瓶颈再引入 Redis。不要一上来就上 Redis否则数据一致性问题会让你花更多时间排查。5. 避坑与排查源码跑不起来时先看这五条5.1 导入数据库后中文乱码现象科室名、医生名显示成问号。原因建库时字符集不是utf8mb4或者连接字符串没指定字符集。解决建库用CREATE DATABASE xxx DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;连接串加?characterEncodingutf8。如果已经建了表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4;逐表改。5.2 号源状态更新了但订单没生成现象用户看到“预约成功”但“我的预约”里没有。原因锁定号源和创建订单不在同一个事务里或者创建订单失败后没有回滚号源状态。解决把UPDATE schedule_source和INSERT appointment_order放在同一个with db.transaction()里任何一步失败都抛异常回滚。5.3 取消预约后号源没释放现象用户取消后该号仍然显示“已约”。原因取消接口只改了订单状态忘了改号源状态或者改号源时WHERE条件写错。解决检查取消逻辑里是否有UPDATE schedule_source SET status0 WHERE idorder.source_id并且确认order.source_id不为空。如果用了 Redis还要把号源 ID 重新加回可约集合。5.4 定时任务重复释放同一个号现象一个号被释放两次导致两个用户抢到同一个号。原因定时任务没有加锁多个实例同时扫到同一批超时订单。解决用SELECT ... FOR UPDATE或 Redis 分布式锁保证同一订单只被一个实例处理。另外释放前再查一次订单状态如果已经不是待支付就跳过。5.5 支付回调重复通知导致重复预约现象用户支付一次但订单状态被改了两次号源被重复占用。原因支付回调没有做幂等。解决在回调入口先查订单状态如果已经是“已预约”就直接返回成功。更新时用WHERE status0限制保证只有待支付订单能被更新。6. 进阶技巧用状态机 对账脚本守住最后一道门6.1 把订单状态流转写成一张表与其在代码里到处写if status 0不如把允许的流转定义成配置。比如ORDER_STATUS_FLOW { 0: [1, 2, 4], # 待支付 - 已预约/已取消/已过期 1: [2, 3], # 已预约 - 已取消/已完成 2: [], # 已取消 终态 3: [], # 已完成 终态 4: [], # 已过期 终态 } def can_transition(from_status, to_status): return to_status in ORDER_STATUS_FLOW.get(from_status, [])这样每次改状态前先调can_transition不合法就拒绝。好处是状态机集中管理新增状态时只改一个地方。6.2 每天跑一次对账脚本再好的代码也可能因为异常、超时、人工干预出现数据不一致。我习惯每天凌晨跑一个对账脚本检查三件事订单状态为“已预约”但号源状态不是“已约”的记录号源状态为“已约”但订单状态不是“已预约”的记录锁定超过 1 小时但订单还是“待支付”的记录。前两类直接告警人工处理第三类自动释放。-- 对账查询1订单已预约但号源不是已约 SELECT o.order_no, o.source_id, s.status AS source_status FROM appointment_order o JOIN schedule_source s ON s.id o.source_id WHERE o.status 1 AND s.status ! 1; -- 对账查询2号源已约但订单不是已预约 SELECT s.id AS source_id, o.order_no, o.status AS order_status FROM schedule_source s LEFT JOIN appointment_order o ON o.id s.order_id WHERE s.status 1 AND (o.id IS NULL OR o.status ! 1);这两条 SQL 跑出来如果有结果说明数据已经不一致了。不要等到用户投诉才发现。6.3 一个我踩过的坑别在事务里调外部接口早期我在锁定号源的事务里调了短信接口发验证码结果短信服务超时导致整个事务回滚号源没锁上用户以为预约失败。后来改成先提交事务再异步发短信。事务里只做数据库操作任何外部调用都放到事务外。这个习惯帮我省了很多后悔药。6.4 验证方法用 JMeter 模拟 50 个并发抢同一个号想验证乐观锁是否生效可以用 JMeter 或 wrk 对锁定接口发 50 个并发请求看是否只有一个成功。如果成功数大于 1说明锁没起作用。检查UPDATE ... WHERE version%s AND status0是否真的带上了版本号以及数据库隔离级别是否是REPEATABLE READ。如果用的是 Redis 预扣检查SREM返回值是否被正确判断。最后说一个我的习惯每次改完号源相关逻辑一定会手动跑一遍“锁定→支付→取消→再锁定”的完整链路并且查一次对账 SQL。这个习惯比写单元测试还管用因为很多问题只在状态流转时出现。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

端到端目标检测Transformer:原理、汇报演示与避坑指南

端到端目标检测Transformer:原理、汇报演示与避坑指南

简介:这套文献汇报PPT聚焦 Facebook AI 提出的 DETR(DEtection TRansformer)目标检测方法,面向机器学习、计算机视觉方向的研究生与开发者,尤其适合正在精读原版论文或准备组会汇报的读者,帮助理解如何利用…

📅 2026/10/15 22:11:03
回形针设计原理与创意改造:从夹文件到临时工具的全场景指南

回形针设计原理与创意改造:从夹文件到临时工具的全场景指南

回形针这东西,大概是最被低估的办公用品,没有之一。你办公桌上可能就有一盒,但你大概率只拿它夹个文件,然后就忘了它的存在。我玩这个“paperclip”主题的时间不短,从单纯的文书整理工具,到手工改造、到应急…

📅 2026/10/15 22:11:03
Windows本地部署n8n全攻略:Docker方案与踩坑实录

Windows本地部署n8n全攻略:Docker方案与踩坑实录

最近不少朋友跟我聊起本地自动化工具,话题总是绕不开 n8n。花了两三个晚上,我把自己 Windows 机器上的 n8n 完整跑起来了,从安装到跑通第一个工作流,中间踩了不少坑,也把整个部署链路彻底理顺了。如果你也想在 Windows…

📅 2026/10/15 22:11:03
MORE NEWS

更多资讯

📰

Uniswap v4交易量过半:DEX换了套玩法,项目方的机会和雷都在这里

过去30天,Uniswap v4的交易量超过440亿美元,占了整个协议交易量的49.9%,跟v3的44.9%就差零点几个点。很多人看到这个数字,第一反应是“版本更新换代了”。但对在Web3做事的人来说,真正该看的不是“v4赢了”&#xff0c…

📰

LabelMe转PaddleOCR格式转换脚本实践:从JSON到TXT标注全攻略

1. 为什么需要格式转换:OCR数据标注的常见困境接触过OCR训练流程的人应该都经历过这个阶段:图片收集了一大批,标注工具选定LabelMe,一个框一个框画了好几宿,终于把几百张图标完。兴冲冲去跑PaddleOCR训练脚本&#xff…

📰

Link prefetch预加载实战:H5性能优化从首屏到下一屏

做H5页面性能优化这几年,我自己踩过最深的坑,是把首屏调得飞快,结果用户点进下一屏还是白白等了两三秒。H5的Link prefetch这项预加载技术,解决的就是这种“下一屏慢”的问题:浏览器在当前页面空闲的时候,提…

📰

R语言dplyr核心函数group_by:分组聚合、分组残留与实战用法详解

接手一份订单明细表的时候,我最先做的不是急着算汇总,而是先把 dplyr 包加载进来,然后对数据集执行 group_by() 分组。这个习惯是踩了无数次坑之后换来的。说白了,group_by() 的核心作用就是把整个数据集按照给定变量切成逻辑上的…

📰

电力行业国产化ERP成本管理方案:从核算到管控的落地实践

电力行业的ERP建设,一直是个“看着标准化、落地极复杂”的领域。特别是成本管理模块,发、输、配、售四个环节的业务逻辑差异巨大,资产密集程度在全部工业门类里都排在最前列,加上电价监管对成本分摊的精细化要求,传统通…

📰

软件工程教程第2版课后习题答案怎么用?计算题推导与复习索引指南

简介:本资源为《软件工程教程》(第2版)(吴迪、马宏茹、丁万宁主编)的课后习题参考答案,面向高校软件工程课程学生及备考复习者,帮助对照教材章节核验选择题、填空题与简答题的作答思路。压缩包内共1个doc文档&#xff…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬