资讯详情 Python医院挂号系统源码:高并发号源锁定与防超卖设计
📅 2026/10/10 23:15:18
简介这份资源是基于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 门诊位置 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生排班表定义某医生某天上午/下午出诊 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 号源实例表每个号一条记录用状态控制避免余量字段被并发扣减 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表逻辑的关键在于号源余量不是算出来的而是数出来的。slot表里每个号是一条独立记录扣号时用UPDATE slot SET status1, patient_id? WHERE id? AND status0靠 InnoDB 的行锁和status0条件保证只有一个事务能成功。参数上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 是periodvalue 是剩余号数。过期时间设到当天门诊结束后的凌晨比如EXPIREAT到次日 00:30。注意Redis 里的余量只用于展示和预校验真正扣号必须落库否则缓存和数据库不一致时会出现“页面显示有号点进去约不上”的玄学问题。3. 号源锁定与并发扣减用 Python 把超卖概率压到零3.1 悲观锁、乐观锁和唯一索引挂号场景该用哪个并发扣号有三种常见做法。悲观锁SELECT ... FOR UPDATE简单但会把行锁持有到事务提交早高峰时容易排队。乐观锁用版本号冲突时重试适合冲突不激烈的场景。挂号系统的冲突恰恰集中在热门专家号上重试次数会很高。我更推荐“条件更新 唯一索引”的组合UPDATE slot SET status1 WHERE id? AND status0影响行数为 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: # 条件更新只有 status0 的行会被改影响行数即结果 result session.execute( update(Slot) .where( Slot.schedule_id schedule_id, Slot.slot_no slot_no, Slot.status 0 # 关键条件防止重复扣减 ) .values(status1, patient_idpatient_id) ) if result.rowcount 0: return False, 该号源已被预约请选择其他号 # 写入预约记录唯一索引兜底防重复 session.add(Appointment( schedule_idschedule_id, slot_noslot_no, patient_idpatient_id, status1 )) 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 status0 WHERE status2因为要区分“锁定”和“已约”。我一般把锁定状态设为 2支付成功后改为 1超时任务只扫status2 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(minutes15): 释放超时未支付的锁定号源 deadline datetime.now() - timedelta(minutesminutes) with session_scope() as session: result session.execute( update(Slot) .where( Slot.status 2, Slot.lock_time deadline ) .values(status0, patient_idNone, lock_timeNone) ) 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(hostlocalhost, port6379, db0) def rate_limit(key, limit1, window1): 简单计数器限流返回 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 frl:grab:{patient_id} if not rate_limit(key, limit1, window1): return jsonify({code: 429, msg: 操作过于频繁请稍后再试}) # ... 继续调用 grab_slot参数上limit1表示每秒最多 1 次window1表示窗口 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_idsch.id, slot_noi, status0) 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字段停诊时把排班标记为停诊同时把该排班下status0的号源改为 3停诊status1的号源保留并触发通知。# 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(status0) # 0停诊 1正常 ) result session.execute( update(Slot) .where( Slot.schedule_id schedule_id, Slot.status 0 ) .values(status3) ) return result.rowcount参数说明status3表示停诊前端查询号源时要过滤掉。已约的号源不在这里处理而是由通知模块读取status1的记录给患者发短信或站内信。这个流程在课程设计里经常被简化成直接删除排班但那样会导致患者预约记录变成孤儿数据答辩时容易被问住。4.3 取消预约释放号源和防重复取消患者取消预约时要把slot状态从 1 改回 0同时把Appointment记录标记为已取消。这里的关键是防重复取消如果患者连点两次取消第一次已经把status改成 0第二次条件更新status1会失败直接返回“已取消”。# 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(status0, patient_idNone, lock_timeNone) ) 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(status2) # 2已取消 ) return True, 取消成功参数上patient_id必须参与条件防止 A 患者取消 B 患者的号。status1条件保证只有已约状态能取消锁定状态2不能直接取消要走超时释放。这个细节在压测时能避免大量脏数据。5. 避坑与排查挂号系统上线后最容易翻车的 5 个点5.1 现象页面显示有号点击预约却提示无号原因Redis 缓存余量和数据库slot表状态不一致。常见于释放超时号源后只更新了数据库没删缓存或者缓存过期时间设得太长。解决释放任务和取消接口在更新数据库后必须删除对应schedule:remain:{schedule_id}缓存查询余量时以数据库COUNT(status0)为准缓存只做展示加速扣号前再校验一次。5.2 现象早高峰数据库连接数暴涨接口超时原因每个请求都新建数据库连接或者连接池太小。Flask SQLAlchemy 默认连接池是 5早高峰不够用。解决把pool_size调到 20max_overflow调到 40同时加接口限流。注意连接池不是越大越好超过数据库max_connections会直接报错一般按数据库最大连接数 / 应用实例数来估。5.3 现象同一患者出现两条预约记录原因Appointment表没有唯一索引或者抢号接口没有做幂等。解决在Appointment上加UNIQUE(schedule_id, slot_no)抢号时先查是否已有记录或者直接用INSERT ... ON DUPLICATE KEY UPDATE。更稳妥的是在slot表条件更新成功后再插入预约记录靠事务保证一致性。5.4 现象超时释放任务跑完后号源数量对不上原因释放任务把status2的号改成 0但有些号其实已经支付成功、状态是 1被误改。解决释放条件必须严格限定status2 AND lock_time deadline不能只按时间过滤。另外支付回调要把status从 2 改成 1并清除lock_time避免被释放任务扫到。5.5 现象医生停诊后患者还能约到停诊号原因停诊时只改了排班状态没改号源状态或者前端查询没过滤status3。解决停诊操作要同时更新schedule.status和该排班下slot.status0的记录为 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_id1 AND status1结果必须是 100不能多也不能少。如果超过 100说明扣号逻辑有并发漏洞如果少于 100说明有号被锁定但没释放或者限流太严。6.2 验证清单上线前必须确认的 4 个数据一致性检查压测通过后还要做几项数据检查。第一slot表里同一schedule_id下status1的数量不能超过total_slots。第二Appointment表里同一schedule_id slot_no只能有一条有效记录。第三Redis 里的余量和数据库COUNT(status0)的差值不能超过 1因为缓存有延迟。第四超时释放任务跑完后status2的记录数应该为 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是最后一道防线。即使代码里条件更新写错了唯一索引也会让重复插入失败事务回滚不会产生脏数据。这个习惯让我在多次压测和线上问题里省掉了大量排查时间。希望帮到你。本文还有配套的精品资源点击获取