news 2026/10/10 23:15:10

Python医院挂号系统源码:高并发号源锁定与防超卖设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python医院挂号系统源码:高并发号源锁定与防超卖设计

简介:这份资源是基于Python的医院门诊挂号与预约系统设计源码,面向计算机相关专业的毕业设计、课程设计学生,以及需要搭建医疗信息系统原型的开发者。项目采用前后端分离架构,前端以Vue组件构建模块化界面,后端用Python脚本处理业务逻辑,并借助TypeScript增强类型检查与代码健壮性,可帮助读者理解挂号、预约等核心流程的完整实现路径。压缩包共220个文件,约8.39MB,包含32个Python脚本、24个TypeScript文件、22个Vue组件、21个JavaScript脚本,以及39个SVG、37个JPEG、14个PNG等图片素材,另有SQL建表脚本、JSON配置、LESS样式、Markdown与docx文档及WOFF字体,覆盖前后端代码、数据库结构与界面资源。目前已有362人学习。读者可据此获得一套结构清晰、可直接参考的赛题级方案,用于快速搭建环境、梳理目录组织与排错思路。

1. 门诊挂号系统为什么总在早高峰崩:从一份 Python 源码说起

早上七点半,挂号窗口还没开,手机端已经涌进上千次请求,数据库连接池瞬间打满,排班表被反复读取却几乎不变——这是很多医院门诊挂号与预约系统上线后第一个月必然遇到的场景。基于 Python 的医院门诊挂号与预约系统设计源码,本质上要解决的就是这类高并发读、强一致性写、号源不能超卖的问题。它适合两类人:一类是计算机专业做课程设计或毕业设计的学生,需要一套能跑通、能讲清楚业务闭环的完整工程;另一类是小团队开发者,想拿一套可读性强的 Python 后端作为院内轻量级挂号服务的起点。这套源码通常包含患者端、医生排班、号源池、预约锁号和取消释放几个核心模块,技术栈以 Flask 或 Django 为主,数据库多用 MySQL,缓存层用 Redis 兜住早高峰。下面我按实际落地顺序,把选型、建表、锁号、排班、压测和踩坑一条条拆开讲。

2. 技术选型与数据库表结构:Flask 还是 Django,号源表怎么建

2.1 框架选型:为什么门诊挂号场景我更倾向 Flask + SQLAlchemy

课程设计里最常见的争论是 Flask 和 Django 二选一。Django 自带 Admin 和 ORM,开箱即用,排班表、科室表、医生表可以直接在后台维护,适合功能全但并发不高的院内系统。Flask 更轻,路由和扩展自己拼,配合 SQLAlchemy 和 Redis 做号源扣减时,控制粒度更细,压测时也更容易定位瓶颈。我一般会这样选:如果源码要交付给非技术老师演示,Django 的 Admin 能省掉一半前端工作量;如果重点在“号源不超卖”这个技术点上,Flask 更透明。

实际落地时,挂号系统的读请求远大于写请求。科室列表、医生简介、未来七天排班这些数据一天可能被读几十万次,但一天只变一次。所以架构上必须把“读”和“写”分开:排班和号源余量放 Redis,扣号走数据库事务,查号直接读缓存。

2.2 核心表结构:号源表、排班表、预约记录表怎么设计

挂号系统最容易翻车的地方是表结构。很多人把“号源余量”直接存在排班表的一个字段里,扣号时UPDATE schedule SET remain = remain - 1,看起来简单,但一旦并发上来,超卖和负数余量就出现了。正确做法是把号源拆成“排班定义”和“号源实例”两层。

-- 科室表 CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '科室名称', location VARCHAR(128) COMMENT '门诊位置' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 医生排班表:定义某医生某天上午/下午出诊 CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, dept_id INT NOT NULL, work_date DATE NOT NULL COMMENT '出诊日期', period TINYINT NOT NULL COMMENT '1上午 2下午', total_slots INT NOT NULL COMMENT '总号源数', fee DECIMAL(8,2) NOT NULL COMMENT '挂号费', UNIQUE KEY uk_doctor_date_period (doctor_id, work_date, period) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 号源实例表:每个号一条记录,用状态控制,避免余量字段被并发扣减 CREATE TABLE slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id INT NOT NULL, slot_no INT NOT NULL COMMENT '第几号', status TINYINT NOT NULL DEFAULT 0 COMMENT '0可约 1已约 2锁定 3停诊', patient_id INT DEFAULT NULL, lock_time DATETIME DEFAULT NULL COMMENT '锁定时间,用于超时释放', UNIQUE KEY uk_schedule_slot (schedule_id, slot_no), KEY idx_status_lock (status, lock_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段建表逻辑的关键在于:号源余量不是算出来的,而是数出来的。slot表里每个号是一条独立记录,扣号时用UPDATE slot SET status=1, patient_id=? WHERE id=? AND status=0,靠 InnoDB 的行锁和status=0条件保证只有一个事务能成功。参数上,status用 0/1/2/3 四个状态而不是布尔值,是为了支持“锁定后超时释放”和“医生临时停诊”。lock_time配合定时任务,可以把超过 15 分钟未支付的号自动退回可约状态,这是防止号源被恶意占用的后悔药。

提示:slot表在热门科室可能一天生成几百条记录,索引idx_status_lock是给超时释放任务用的,没有它,定时扫描会全表扫,凌晨跑任务时数据库 IO 会飙高。

2.3 缓存层:Redis 存什么、过期时间怎么定

排班和余量放 Redis 后,读接口的 QPS 能从几百提到几千。我一般用 Hash 结构存某个排班下的余量:key 是schedule:remain:{schedule_id},field 是period,value 是剩余号数。过期时间设到当天门诊结束后的凌晨,比如EXPIREAT到次日 00:30。注意,Redis 里的余量只用于展示和预校验,真正扣号必须落库,否则缓存和数据库不一致时会出现“页面显示有号,点进去约不上”的玄学问题。

3. 号源锁定与并发扣减:用 Python 把超卖概率压到零

3.1 悲观锁、乐观锁和唯一索引,挂号场景该用哪个

并发扣号有三种常见做法。悲观锁SELECT ... FOR UPDATE简单,但会把行锁持有到事务提交,早高峰时容易排队。乐观锁用版本号,冲突时重试,适合冲突不激烈的场景。挂号系统的冲突恰恰集中在热门专家号上,重试次数会很高。我更推荐“条件更新 + 唯一索引”的组合:UPDATE slot SET status=1 WHERE id=? AND status=0,影响行数为 1 才算抢到,为 0 直接返回“号已被约”。这样不需要显式加锁,InnoDB 的行锁在更新时自动生效,代码也短。

# slot_service.py from sqlalchemy import update from models import Slot, Appointment from db import session_scope def grab_slot(schedule_id, slot_no, patient_id): """抢号:条件更新 + 事务,返回 (是否成功, 提示)""" with session_scope() as session: # 条件更新:只有 status=0 的行会被改,影响行数即结果 result = session.execute( update(Slot) .where( Slot.schedule_id == schedule_id, Slot.slot_no == slot_no, Slot.status == 0 # 关键条件,防止重复扣减 ) .values(status=1, patient_id=patient_id) ) if result.rowcount == 0: return False, "该号源已被预约,请选择其他号" # 写入预约记录,唯一索引兜底防重复 session.add(Appointment( schedule_id=schedule_id, slot_no=slot_no, patient_id=patient_id, status=1 )) return True, "预约成功"

这段代码的逻辑说明:session_scope()保证整个操作在一个事务里,update的where条件里带status == 0,数据库层面只会让一个事务的rowcount为 1。参数上,schedule_id和slot_no联合定位号源,patient_id用于后续取消和查询。如果rowcount为 0,说明号已经被别人抢走,直接返回失败,不需要重试。Appointment表上要加UNIQUE(schedule_id, slot_no),这样即使代码有 bug,数据库也会拦住重复预约。

3.2 锁定与支付超时释放:定时任务怎么写才不拖垮数据库

抢到号不等于完成预约,很多系统要求 15 分钟内支付或确认,否则释放。释放逻辑不能直接UPDATE slot SET status=0 WHERE status=2,因为要区分“锁定”和“已约”。我一般把锁定状态设为 2,支付成功后改为 1,超时任务只扫status=2 AND lock_time < NOW() - INTERVAL 15 MINUTE。

# release_task.py from datetime import datetime, timedelta from sqlalchemy import update from models import Slot from db import session_scope def release_expired_slots(minutes=15): """释放超时未支付的锁定号源""" deadline = datetime.now() - timedelta(minutes=minutes) with session_scope() as session: result = session.execute( update(Slot) .where( Slot.status == 2, Slot.lock_time < deadline ) .values(status=0, patient_id=None, lock_time=None) ) return result.rowcount

参数说明:minutes默认 15,可按医院规则调整;lock_time在抢号时写入datetime.now()。这个任务建议每 1 分钟跑一次,但不要用SELECT再逐条UPDATE,那样在号源多的时候会产生大量小事务。直接一条批量UPDATE,配合idx_status_lock索引,几千条记录的释放能在几十毫秒内完成。注意,释放后要同步删除 Redis 里的对应缓存,否则页面余量会偏小,患者看到“无号”但其实有号,这种血泪经验在压测时特别容易被忽略。

3.3 接口层限流:为什么早高峰要先挡掉重复点击

即使扣号逻辑正确,早高峰的重复点击也会把数据库连接打满。我一般会在 Flask 层加一个基于 Redis 的令牌桶或简单计数器,对同一患者 ID 的挂号接口限制每秒 1 次,对同一 IP 限制每秒 5 次。实现可以用INCR加EXPIRE,也可以用redis-py的pipeline。

# rate_limit.py import redis from flask import request, jsonify r = redis.Redis(host='localhost', port=6379, db=0) def rate_limit(key, limit=1, window=1): """简单计数器限流,返回 True 表示放行""" current = r.incr(key) if current == 1: r.expire(key, window) return current <= limit # 在挂号路由里调用 def grab_route(): patient_id = request.json.get('patient_id') key = f"rl:grab:{patient_id}" if not rate_limit(key, limit=1, window=1): return jsonify({"code": 429, "msg": "操作过于频繁,请稍后再试"}) # ... 继续调用 grab_slot

参数上,limit=1表示每秒最多 1 次,window=1表示窗口 1 秒。这个限流不是用来防攻击的,而是挡住用户手抖连点。真正的大流量需要网关层做,但课程设计和小团队项目里,这一层已经能明显降低数据库压力。

4. 排班管理与预约流程:从医生停诊到患者取消的完整闭环

4.1 排班生成:批量插入号源实例的 Python 脚本

排班表建好后,需要根据total_slots生成对应数量的slot记录。常见做法是医生排班保存时同步生成,但更稳妥的是用定时任务每天凌晨生成未来第 7 天的号源,避免排班修改导致号源错乱。

# generate_slots.py from models import Schedule, Slot from db import session_scope def generate_slots_for_date(work_date): """为指定日期所有排班生成号源实例""" with session_scope() as session: schedules = session.query(Schedule).filter( Schedule.work_date == work_date ).all() for sch in schedules: # 先检查是否已生成,避免重复 exists = session.query(Slot).filter( Slot.schedule_id == sch.id ).first() if exists: continue slots = [ Slot(schedule_id=sch.id, slot_no=i, status=0) for i in range(1, sch.total_slots + 1) ] session.bulk_save_objects(slots) return len(schedules)

逻辑说明:bulk_save_objects比逐条add快很多,几百条号源能在一次事务里插入。参数上,work_date是目标日期,total_slots来自排班表。注意,生成前要检查是否已存在,否则任务重跑会产生重复号源,uk_schedule_slot唯一索引会报错,但更优雅的是先查后插。

4.2 停诊与改期:医生临时停诊时号源怎么处理

医生停诊是挂号系统里最容易被忽略的异常流程。处理原则是:已预约的号不能直接删,要通知患者并支持改期或退费。源码里一般会有一个schedule_status字段,停诊时把排班标记为停诊,同时把该排班下status=0的号源改为 3(停诊),status=1的号源保留并触发通知。

# suspend_schedule.py from sqlalchemy import update from models import Schedule, Slot from db import session_scope def suspend_schedule(schedule_id): """医生停诊:可约号源置为停诊,已约号源保留待处理""" with session_scope() as session: session.execute( update(Schedule) .where(Schedule.id == schedule_id) .values(status=0) # 0停诊 1正常 ) result = session.execute( update(Slot) .where( Slot.schedule_id == schedule_id, Slot.status == 0 ) .values(status=3) ) return result.rowcount

参数说明:status=3表示停诊,前端查询号源时要过滤掉。已约的号源不在这里处理,而是由通知模块读取status=1的记录,给患者发短信或站内信。这个流程在课程设计里经常被简化成直接删除排班,但那样会导致患者预约记录变成孤儿数据,答辩时容易被问住。

4.3 取消预约:释放号源和防重复取消

患者取消预约时,要把slot状态从 1 改回 0,同时把Appointment记录标记为已取消。这里的关键是防重复取消:如果患者连点两次取消,第一次已经把status改成 0,第二次条件更新status=1会失败,直接返回“已取消”。

# cancel_service.py from sqlalchemy import update from models import Slot, Appointment from db import session_scope def cancel_appointment(schedule_id, slot_no, patient_id): """取消预约:条件更新,防止重复取消""" with session_scope() as session: result = session.execute( update(Slot) .where( Slot.schedule_id == schedule_id, Slot.slot_no == slot_no, Slot.patient_id == patient_id, Slot.status == 1 # 只有已约状态才能取消 ) .values(status=0, patient_id=None, lock_time=None) ) if result.rowcount == 0: return False, "预约不存在或已取消" session.execute( update(Appointment) .where( Appointment.schedule_id == schedule_id, Appointment.slot_no == slot_no, Appointment.patient_id == patient_id ) .values(status=2) # 2已取消 ) return True, "取消成功"

参数上,patient_id必须参与条件,防止 A 患者取消 B 患者的号。status=1条件保证只有已约状态能取消,锁定状态(2)不能直接取消,要走超时释放。这个细节在压测时能避免大量脏数据。

5. 避坑与排查:挂号系统上线后最容易翻车的 5 个点

5.1 现象:页面显示有号,点击预约却提示无号

原因:Redis 缓存余量和数据库slot表状态不一致。常见于释放超时号源后只更新了数据库,没删缓存,或者缓存过期时间设得太长。解决:释放任务和取消接口在更新数据库后,必须删除对应schedule:remain:{schedule_id}缓存;查询余量时以数据库COUNT(status=0)为准,缓存只做展示加速,扣号前再校验一次。

5.2 现象:早高峰数据库连接数暴涨,接口超时

原因:每个请求都新建数据库连接,或者连接池太小。Flask + SQLAlchemy 默认连接池是 5,早高峰不够用。解决:把pool_size调到 20,max_overflow调到 40,同时加接口限流。注意,连接池不是越大越好,超过数据库max_connections会直接报错,一般按数据库最大连接数 / 应用实例数来估。

5.3 现象:同一患者出现两条预约记录

原因:Appointment表没有唯一索引,或者抢号接口没有做幂等。解决:在Appointment上加UNIQUE(schedule_id, slot_no),抢号时先查是否已有记录,或者直接用INSERT ... ON DUPLICATE KEY UPDATE。更稳妥的是在slot表条件更新成功后再插入预约记录,靠事务保证一致性。

5.4 现象:超时释放任务跑完后,号源数量对不上

原因:释放任务把status=2的号改成 0,但有些号其实已经支付成功、状态是 1,被误改。解决:释放条件必须严格限定status=2 AND lock_time < deadline,不能只按时间过滤。另外,支付回调要把status从 2 改成 1,并清除lock_time,避免被释放任务扫到。

5.5 现象:医生停诊后,患者还能约到停诊号

原因:停诊时只改了排班状态,没改号源状态,或者前端查询没过滤status=3。解决:停诊操作要同时更新schedule.status和该排班下slot.status=0的记录为 3;查询接口的WHERE条件里必须带Slot.status = 0,不能只查排班。

6. 压测与验证:用 Locust 把早高峰场景跑一遍

6.1 压测脚本:模拟 500 人同时抢 100 个号

写完扣号逻辑,不压测就上线等于赌运气。我一般用 Locust 写一个抢号场景,模拟 500 个并发用户抢同一个排班的 100 个号,看最终成功数是不是正好 100,有没有负数余量。

# locustfile.py from locust import HttpUser, task, between import random class GrabUser(HttpUser): wait_time = between(0.1, 0.5) @task def grab(self): # 随机选一个号,模拟真实抢号分布 slot_no = random.randint(1, 100) self.client.post("/api/grab", json={ "schedule_id": 1, "slot_no": slot_no, "patient_id": random.randint(10000, 99999) })

参数说明:wait_time设 0.1 到 0.5 秒,模拟用户快速点击;slot_no随机 1 到 100,让冲突集中在部分号上。跑完后查数据库:SELECT COUNT(*) FROM slot WHERE schedule_id=1 AND status=1,结果必须是 100,不能多也不能少。如果超过 100,说明扣号逻辑有并发漏洞;如果少于 100,说明有号被锁定但没释放,或者限流太严。

6.2 验证清单:上线前必须确认的 4 个数据一致性检查

压测通过后,还要做几项数据检查。第一,slot表里同一schedule_id下status=1的数量不能超过total_slots。第二,Appointment表里同一schedule_id + slot_no只能有一条有效记录。第三,Redis 里的余量和数据库COUNT(status=0)的差值不能超过 1,因为缓存有延迟。第四,超时释放任务跑完后,status=2的记录数应该为 0,除非有正在支付的订单。

-- 检查超卖 SELECT schedule_id, COUNT(*) AS sold FROM slot WHERE status = 1 GROUP BY schedule_id HAVING sold > (SELECT total_slots FROM schedule WHERE id = schedule_id); -- 检查重复预约 SELECT schedule_id, slot_no, COUNT(*) AS cnt FROM appointment WHERE status = 1 GROUP BY schedule_id, slot_no HAVING cnt > 1;

这两条 SQL 建议做成定时巡检,每 5 分钟跑一次,发现异常立即告警。课程设计里不一定有告警系统,但至少要在答辩前跑一遍,确保数据干净。

6.3 一个具体技巧:用数据库唯一索引兜住所有并发漏洞

最后说一个我踩过坑之后养成的习惯:不管代码写得多严谨,一定要在数据库层加唯一索引兜底。slot表的uk_schedule_slot和appointment表的uk_schedule_slot是最后一道防线。即使代码里条件更新写错了,唯一索引也会让重复插入失败,事务回滚,不会产生脏数据。这个习惯让我在多次压测和线上问题里省掉了大量排查时间。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 23:14:14

拆解O奖论文2229059:数学建模中的时间序列预测与交易策略闭环

简介&#xff1a;来自2022年美国大学生数学建模竞赛&#xff08;MCM/ICM&#xff09;C题杰出奖&#xff08;Outstanding Winner&#xff09;的英文原版论文&#xff0c;收录于优秀论文集。内容面向数学建模参赛者、量化交易学习者和高校指导教师&#xff0c;适合研究O奖论文的选…

作者头像 李华
网站建设 2026/10/10 23:07:09

VS Code的C/C++ IntelliSense失灵怎么办?从配置原理到实战排查

“VSCode装好了C/C插件&#xff0c;IntelliSense却像个木头一样&#xff0c;敲了半天代码一个提示都不弹”——这大概是C/C开发者日常里最让人恼火的场景之一。其他语言补全得飞起&#xff0c;一到C/C就哑火&#xff0c;头文件路径报红一片&#xff0c;跳转定义也没反应&#x…

作者头像 李华
网站建设 2026/10/10 22:59:57

海外仓费用高吗?跨境卖家履约成本拆解

很多卖家一听海外仓&#xff0c;本能反应是贵。尤其做大件、重货的&#xff0c;一算头程加仓租加尾程&#xff0c;觉得不如直邮小包划算&#xff0c;于是继续走小包。但贵不贵要看怎么算&#xff0c;不能只比一个数字。本文把海外仓成本拆成几块&#xff0c;再给一组对比逻辑&a…

作者头像 李华
网站建设 2026/10/10 22:56:49

两节点电力系统高斯-赛德尔潮流计算:MATLAB实现与常见坑解析

潮流计算是电力系统分析里绕不开的一步。今天聊一个很有意思的入门题目&#xff1a;两节点电力系统的高斯-赛德尔&#xff08;Gauss-Seidel&#xff09;潮流计算&#xff0c;用MATLAB把PQ节点&#xff08;母线2&#xff09;的电压幅值和相角求出来。这个例子虽然网络规模小到只…

作者头像 李华
网站建设 2026/10/10 22:56:44

OpenClaw 中 Tool 与 Skill 完整异同解析:从 SKILL.md 到 Plugin 的配置验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华