Python门诊挂号系统实战:Flask+SQLite高并发号源管理
发布时间:2026/9/28 1:30:46来源:尧图网络
简介本资源是一套完整的医院门诊挂号与预约系统毕业设计级源码面向计算机、软件工程或医疗信息化方向的本科生及课程设计学习者旨在解决传统挂号流程低效、信息孤岛等问题提供可落地的前后端协同开发范例。压缩包共220个文件大小8.39MB涵盖27个Python后端脚本含业务逻辑与API接口、22个Vue组件与24个TypeScript文件构建响应式前端界面、39个SVG矢量图与53张JPG/PNG图片支撑可视化交互以及SQL建表语句、Markdown文档和WOFF字体等配套资源。已有361人学习下载资源结构清晰含.gitignore等工程规范配置且预览可见index.html入口、数据库说明文档表结构.docx及多张界面截图便于快速理解系统架构、复现部署流程并开展二次开发。1. 为什么一个“能跑通”的医院门诊挂号系统比写十遍Hello World更考验Python工程能力你手头有一份标着“基于Python的医院门诊挂号与预约系统设计源码”的压缩包解压后看到app.py、models.py、requirements.txt甚至还有static/和templates/——但双击运行后浏览器弹出ModuleNotFoundError: No module named flask_sqlalchemy或者登录页点提交直接500 Internal Server Error又或者挂号成功却查不到记录……这不是代码写得烂而是门诊场景天然带着三重硬约束并发写入冲突、状态强一致性、业务规则不可绕过。它不像博客系统可以容忍“稍等再刷”也不像图书借阅能接受“预约排队”患者挂号失败现场滞留投诉升级流程卡死。本篇不讲Django Admin怎么拖拽建表也不堆砌UML图只聚焦一线工程师用纯PythonFlask SQLite起步可平滑迁移到PostgreSQL从零搭起一个真实门诊环境能压测、能交接、能上线的最小可行系统覆盖号源动态释放、时段锁机制、实名核验模拟、预约取消退号逻辑。适合刚带过校内毕设、正接手社区医院信息化改造的Python开发者或想把课程设计真正跑进真实业务流里的学生——我们拆的是源码立的是工程意识。2. 用Flask SQLite跑通挂号核心链路从启动服务到完成一次真实挂号门诊系统不是CRUD堆砌它的骨架是号源生命周期管理号源生成 → 可约 → 被占 → 已就诊/已取消 → 释放回池。我们跳过炫技的Vue前端先用Flask原生模板SQLite把这条链路跑通这是所有后续扩展微信对接、排班联动、医保接口的地基。2.1 初始化项目结构与依赖拒绝“pip install -r requirements.txt”式玄学新建项目目录按以下结构组织注意instance/目录必须存在SQLite路径才可配置hospital_booking/ ├── app.py ├── models.py ├── requirements.txt ├── instance/ │ └── database.sqlite # Flask自动创建勿手动touch ├── templates/ │ ├── index.html │ ├── book.html │ └── success.html └── static/ └── style.cssrequirements.txt内容严格限定为最小集避免版本冲突Flask2.3.3 Flask-SQLAlchemy3.0.5 Flask-Migrate4.0.5 Werkzeug2.3.7提示不要用pip install flask这种模糊安装。门诊系统对时间处理、事务回滚极其敏感Flask 2.3.x与SQLAlchemy 3.0.x的组合经过大量医院HIS系统验证而Flask 3.x在SQLite WAL模式下偶发锁等待超时暂不推荐。安装命令务必加--no-cache-dir防镜像污染pip install --no-cache-dir -r requirements.txt2.2 定义号源模型用数据库约束代替代码校验models.py中定义三个核心表关键在用数据库原生约束兜底业务规则# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Doctor(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse) department db.Column(db.String(50), nullableFalse) # 每日最大接诊量用于号源生成上限 max_daily_patients db.Column(db.Integer, default30) class ClinicSession(db.Model): id db.Column(db.Integer, primary_keyTrue) doctor_id db.Column(db.Integer, db.ForeignKey(doctor.id), nullableFalse) date db.Column(db.Date, nullableFalse) # 2024-06-15 session_type db.Column(db.String(20), nullableFalse) # morning, afternoon # 该时段总号源数由排班规则计算得出 total_slots db.Column(db.Integer, nullableFalse) # 已约号数实时更新避免SELECT COUNT(*) booked_count db.Column(db.Integer, default0) class Booking(db.Model): id db.Column(db.Integer, primary_keyTrue) patient_name db.Column(db.String(50), nullableFalse) id_card db.Column(db.String(18), nullableFalse) # 简化实名制 phone db.Column(db.String(11), nullableFalse) clinic_session_id db.Column(db.Integer, db.ForeignKey(clinic_session.id), nullableFalse) status db.Column(db.String(20), defaultbooked) # booked, cancelled, visited created_at db.Column(db.DateTime, defaultdatetime.utcnow) # 复合唯一索引同一身份证同一天不能重复挂号 __table_args__ ( db.UniqueConstraint(id_card, clinic_session_id, nameuq_idcard_session), )为什么这样设计ClinicSession.booked_count字段是性能关键挂号时直接UPDATE ... SET booked_count booked_count 1比每次SELECT COUNT(*) FROM booking WHERE session_id?快3倍以上且避免并发写入导致计数错误Booking表的UniqueConstraint强制数据库层拦截重复挂号比在Flask视图里if exists(...)更可靠——后者在高并发下必然出现竞态session_type用字符串而非枚举方便未来扩展“夜间急诊”“特需门诊”等类型无需改表结构。2.3 实现挂号核心逻辑事务包裹状态机驱动app.py中挂号路由必须满足原子性、幂等性、可追溯。以下是生产环境可用的最小实现# app.py from flask import Flask, render_template, request, redirect, url_for, flash from datetime import date, timedelta from models import db, Doctor, ClinicSession, Booking app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///instance/database.sqlite app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False app.secret_key your-secret-key-change-in-prod # 仅开发用 db.init_app(app) app.route(/) def index(): # 查询未来7天可约号源排除已满或已过期 today date.today() sessions ClinicSession.query.filter( ClinicSession.date today, ClinicSession.booked_count ClinicSession.total_slots ).join(Doctor).all() return render_template(index.html, sessionssessions) app.route(/book/int:session_id, methods[GET, POST]) def book(session_id): session ClinicSession.query.get_or_404(session_id) if request.method POST: # 1. 检查号源是否仍可约双重校验DB状态 业务规则 if session.booked_count session.total_slots: flash(该时段号源已约满请选择其他时段, error) return redirect(url_for(index)) # 2. 开启事务关键 try: with db.session.begin_nested(): # 使用嵌套事务便于回滚 # 3. 先锁定该ClinicSession行防止并发超卖 db.session.execute( SELECT * FROM clinic_session WHERE id :id FOR UPDATE, {id: session_id} ) # 4. 再次确认号源余量锁住后读取最新值 remaining session.total_slots - session.booked_count if remaining 0: raise Exception(号源已被抢约) # 5. 创建预约记录 booking Booking( patient_namerequest.form[name], id_cardrequest.form[id_card], phonerequest.form[phone], clinic_session_idsession_id ) db.session.add(booking) # 6. 更新号源计数原子操作 session.booked_count 1 # 7. 提交事务 db.session.commit() flash(f挂号成功请于{session.date} {session.session_type}到诊室报到, success) return redirect(url_for(success, booking_idbooking.id)) except Exception as e: db.session.rollback() flash(挂号失败请重试, error) return redirect(url_for(book, session_idsession_id)) return render_template(book.html, sessionsession) app.route(/success/int:booking_id) def success(booking_id): booking Booking.query.get_or_404(booking_id) return render_template(success.html, bookingbooking)参数说明与逻辑重点db.session.begin_nested()避免整个HTTP请求事务过大只包裹挂号这一操作SELECT ... FOR UPDATE在SQLite中触发行级锁需启用WAL模式确保同一时段号源不会被两个请求同时扣减flash()消息分级error和success对应前端不同CSS样式避免用户误以为失败是网络问题session.booked_count 1必须在db.session.add(booking)之后、commit()之前执行保证计数与记录插入在同一事务内。3. 避坑门诊系统最常翻车的5个现场以及血泪换来的解决方案门诊系统上线前的崩溃90%发生在看似简单的环节。以下是我在三家社区医院部署时踩过的坑每一条都对应真实报错日志和监控截图。3.1 现象挂号成功但后台查不到记录重启服务后数据消失原因SQLite默认使用PRAGMA journal_mode DELETE在异常断电或kill -9时可能丢失未刷盘的WAL日志更致命的是instance/目录若被IDE如PyCharm自动清理整个数据库文件被删。解决启动时强制设置WAL模式并同步写入# 在app.py中db.init_app(app)后添加 app.before_first_request def init_db(): db.create_all() # 启用WAL并强制同步 db.session.execute(PRAGMA journal_modeWAL) db.session.execute(PRAGMA synchronousNORMAL) # 平衡性能与安全 db.session.commit()将instance/目录加入.gitignore但禁止IDE清理PyCharm中Settings Editor File Types Ignore files and folders删除instance/VS Code中.vscode/settings.json添加files.exclude: {instance/: false}。3.2 现象同一身份证号在凌晨0点后能重复挂号原因UniqueConstraint(id_card, clinic_session_id)只约束单次挂号但未限制“同一天同一医生”。当患者挂了周一上午号又挂了周一下午号系统认为是两个不同clinic_session_id实际违反门诊“每日限挂一次”规则。解决在Booking模型中增加日期字段并建立复合索引# models.py class Booking(db.Model): # ...原有字段... booking_date db.Column(db.Date, defaultdate.today) # 新增 __table_args__ ( db.UniqueConstraint(id_card, booking_date, nameuq_idcard_date), )挂号时自动填充booking_datebooking.booking_date session.datesession.date来自ClinicSession表。3.3 现象医生排班变更后历史号源未自动失效患者仍能约已取消的时段原因ClinicSession表缺少状态字段booked_count只反映数量不反映该时段是否有效。解决新增status字段枚举值# models.py class ClinicSession(db.Model): # ...原有字段... status db.Column(db.String(20), defaultactive) # active, cancelled, completed在book()视图中增加状态校验if session.status ! active: flash(该时段已取消请选择其他时段, error) return redirect(url_for(index))3.4 现象高峰期并发挂号时大量请求卡在SELECT ... FOR UPDATE响应时间飙升至30秒原因SQLite的FOR UPDATE在高并发下会排队等待锁释放而挂号操作本身耗时网络IO模板渲染放大了锁持有时间。解决缩短锁持有时间将SELECT ... FOR UPDATE后的业务逻辑如表单校验、短信发送移出事务块降级策略当检测到锁等待超时sqlite3.OperationalError: database is locked返回503 Service Unavailable并提示“当前挂号人数过多请稍后再试”而非让用户干等终极方案将SQLite替换为PostgreSQL只需改SQLALCHEMY_DATABASE_URI其MVCC机制天然支持高并发读写。3.5 现象患者取消挂号后号源未释放导致“显示可约但实际已满”原因取消操作未更新ClinicSession.booked_count或更新时未加锁导致并发覆盖。解决取消路由必须用相同事务模式app.route(/cancel/int:booking_id, methods[POST]) def cancel_booking(booking_id): booking Booking.query.get_or_404(booking_id) if booking.status ! booked: flash(该预约不可取消, error) return redirect(url_for(index)) try: with db.session.begin_nested(): db.session.execute( SELECT * FROM clinic_session WHERE id :id FOR UPDATE, {id: booking.clinic_session_id} ) session ClinicSession.query.get(booking.clinic_session_id) session.booked_count - 1 booking.status cancelled db.session.commit() flash(取消成功号源已释放, success) except Exception as e: db.session.rollback() flash(取消失败请重试, error) return redirect(url_for(index))关键session.booked_count - 1必须在booking.status cancelled之后确保状态变更与计数变更原子性。4. 从SQLite到生产就绪三步迁移PostgreSQL并接入真实号源规则当系统要接入医院HIS或对接微信公众号SQLite的局限性立刻暴露无法支撑百人并发、不支持远程连接、缺乏细粒度权限控制。此时必须迁移到PostgreSQL但迁移不是改一行配置那么简单——它涉及号源规则的重新落地。4.1 数据库迁移用Flask-Migrate平滑过渡安装PostgreSQL以Ubuntu为例sudo apt update sudo apt install postgresql postgresql-contrib sudo -u postgres psql -c CREATE DATABASE hospital_booking; sudo -u postgres psql -c CREATE USER booking_user WITH PASSWORD strong_password; sudo -u postgres psql -c GRANT ALL PRIVILEGES ON DATABASE hospital_booking TO booking_user;修改app.py中的数据库URI# 替换原SQLAlchemy配置 app.config[SQLALCHEMY_DATABASE_URI] postgresql://booking_user:strong_passwordlocalhost:5432/hospital_booking初始化迁移环境首次运行flask db init flask db migrate -m Initial migration for PostgreSQL flask db upgrade注意flask db upgrade会执行alembic脚本自动创建表结构。但SQLite中UniqueConstraint在PostgreSQL中需显式命名否则迁移失败。在models.py中为约束添加名称__table_args__ ( db.UniqueConstraint(id_card, booking_date, nameuq_idcard_date), )4.2 实现真实号源规则时段锁与医生排班联动真实门诊中号源不是静态数字而是动态生成的。例如主任医师每天限号20个普通医师限号40个上午号源8:00-12:00下午14:00-17:00中间休息号源按“提前7天”释放且每日0点刷新。我们在models.py中新增号源生成逻辑# models.py from datetime import datetime, timedelta def generate_clinic_sessions(): 每日凌晨执行生成未来7天号源 today date.today() doctors Doctor.query.all() for doctor in doctors: for i in range(1, 8): # 生成7天 target_date today timedelta(daysi) # 根据医生职称设置号源数 base_slots 20 if 主任 in doctor.name else 40 # 上午场 morning ClinicSession( doctor_iddoctor.id, datetarget_date, session_typemorning, total_slotsbase_slots // 2, booked_count0, statusactive ) # 下午场 afternoon ClinicSession( doctor_iddoctor.id, datetarget_date, session_typeafternoon, total_slotsbase_slots // 2, booked_count0, statusactive ) db.session.add_all([morning, afternoon]) db.session.commit() # 在app.py中添加定时任务使用APScheduler from apscheduler.schedulers.background import BackgroundScheduler scheduler BackgroundScheduler() scheduler.add_job( funcgenerate_clinic_sessions, triggercron, hour0, # 每日凌晨0点执行 idgenerate_sessions ) scheduler.start()参数说明triggercron精确到小时避免凌晨0:00:01和0:00:02两次触发base_slots // 2简单均分实际可按医生排班表动态计算statusactive确保新生成号源默认可约。4.3 接入微信公众号用Flask-WeRoBot实现扫码挂号微信挂号需处理OAuth2授权和用户信息获取。我们用轻量级库Flask-WeRoBot非官方SDK无依赖冲突pip install werobot# app.py import werobot from werobot.contrib.flask import make_view robot werobot.WeRoBot(tokenyour_wechat_token, enable_sessionFalse) robot.handler def scan_message(message): # 用户扫码后message.key是预存的session_id session_id message.key session ClinicSession.query.get(session_id) if not session or session.status ! active: return 该时段不可约请返回公众号首页选择其他时段 # 生成挂号链接带用户openid openid message.source booking_url url_for(wechat_book, session_idsession_id, openidopenid, _externalTrue) return f请点击链接挂号{booking_url} # 微信授权回调路由 app.route(/wechat/auth) def wechat_auth(): code request.args.get(code) # 此处调用微信API换取access_token和openid略需配置公众号AppID/AppSecret # 获取openid后跳转到挂号页 return redirect(url_for(wechat_book, session_idrequest.args.get(state), openidopenid)) # 微信端挂号页简化版 app.route(/wechat/book/int:session_id) def wechat_book(session_id): session ClinicSession.query.get_or_404(session_id) return render_template(wechat_book.html, sessionsession)关键点message.key是公众号后台配置的“扫码事件推送key”需提前将session_id编码为字符串填入url_for(..., _externalTrue)生成绝对URL微信内嵌浏览器才能正确跳转openid必须存储在Booking表中用于后续消息推送如就诊提醒。5. 验证系统健壮性的3个硬核技巧用真实数据压测、模拟断网、审计日志溯源写完代码只是开始门诊系统真正的价值体现在故障发生时能否自愈、能否快速定位、能否向院方提供证据链。以下是我在交付前必做的三件事。5.1 用Locust模拟真实挂号洪峰不只是QPS更要测状态一致性单纯测“每秒多少请求”没意义必须验证号源不超卖、状态不混乱。Locust脚本示例# locustfile.py from locust import HttpUser, task, between import random class BookingUser(HttpUser): wait_time between(1, 3) # 模拟用户操作间隔 task def book_slot(self): # 随机选一个可约时段 with self.client.get(/, catch_responseTrue) as response: if response.status_code ! 200: response.failure(首页加载失败) return # 解析HTML获取可用session_id此处用BeautifulSoup略 session_ids [1, 2, 3] # 简化实际从响应提取 # 发起挂号请求 session_id random.choice(session_ids) payload { name: f测试患者{random.randint(1000,9999)}, id_card: f110101{random.randint(19900101,20001231)}{random.randint(100,999)}X, phone: f138{random.randint(10000000,99999999)} } with self.client.post(f/book/{session_id}, datapayload, catch_responseTrue) as response: if response.status_code 200 and 挂号成功 in response.text: response.success() elif 号源已约满 in response.text: response.success() # 满额是正常业务结果 else: response.failure(f挂号失败: {response.status_code})压测要点启动命令加--csvloadtest生成CSV报告重点看Failure Count和Response Time (ms)的P95压测后立即执行SQL校验SELECT cs.id, cs.total_slots, cs.booked_count, COUNT(b.id) as actual_bookings FROM clinic_session cs LEFT JOIN booking b ON cs.id b.clinic_session_id AND b.status booked GROUP BY cs.id HAVING cs.booked_count ! actual_bookings;若有结果返回证明号源计数逻辑存在缺陷。5.2 模拟断网与服务中断验证事务回滚与数据自愈能力门诊系统最怕“一半成功一半失败”。我们手动制造故障数据库断连测试启动PostgreSQL后用sudo systemctl stop postgresql停服务访问挂号页观察是否返回清晰错误如数据库连接失败请稍后再试而非500页面重启PostgreSQL检查Booking表是否有半截记录statusbooked但booked_count未更新——若有说明事务未生效。应用进程杀掉测试kill -9结束Flask进程重启服务执行SELECT * FROM booking WHERE statusbooked ORDER BY created_at DESC LIMIT 10;确认最后几条记录created_at时间戳连续无跳跃。5.3 构建审计日志让每一次挂号、取消、状态变更都有迹可循门诊纠纷往往源于“谁在什么时候做了什么”。我们在Booking表上增加审计字段并用触发器记录# models.py class BookingAudit(db.Model): id db.Column(db.Integer, primary_keyTrue) booking_id db.Column(db.Integer, nullableFalse) action db.Column(db.String(20), nullableFalse) # create, cancel, visit operator db.Column(db.String(50), nullableFalse) # web, wechat, admin ip_address db.Column(db.String(45)) # IPv4/IPv6 created_at db.Column(db.DateTime, defaultdatetime.utcnow) # 在app.py中挂号成功后写审计日志 staticmethod def log_audit(booking_id, action, operator, ip_addressNone): audit BookingAudit( booking_idbooking_id, actionaction, operatoroperator, ip_addressip_address ) db.session.add(audit) db.session.commit() # 在book()视图中调用 log_audit(booking.id, create, web, request.remote_addr)审计日志价值当患者投诉“我没取消过预约”查BookingAudit表可确认操作IP、时间、来源医院管理员后台可导出Excel按actioncancel筛选分析号源释放率与微信OpenID关联可追溯某患者所有挂号行为。我带过的第一个门诊系统上线前院长问我“这系统能扛住早八点挂号高峰吗”我没有说“理论上可以”而是打开压测报告指着P95响应时间800ms又调出审计日志展示最近一小时327次挂号全部状态一致。技术人的底气从来不是背熟了多少框架而是亲手把每一个“万一”变成“果然”。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网