news 2026/9/14 2:17:09

Django+Vue医疗预约系统:MySQL事务与高并发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+Vue医疗预约系统:MySQL事务与高并发实战

简介:本资源是一套基于Python Django与Vue.js全栈开发的医疗预约与诊断系统完整实现,面向Web开发初学者及医疗信息化项目实践者,解决传统线下挂号流程低效、医患信息同步滞后、诊疗数据管理分散等痛点。压缩包共689个文件,涵盖147个Vue组件文件(构建响应式前端界面)、53个Python后端模块(含Django视图、模型与API逻辑)、101张JPG/PNG素材与159个SVG图标(支撑UI视觉体系),以及MySQL建库脚本(sql)、自动化部署脚本(bat/sh)和演示视频(mp4),整体大小为105.17MB。目前已有83人学习下载,资源结构清晰,包含从环境搭建(install.bat)、数据库初始化(init_sql.bat)、前后端启动(run.bat)到构建发布的完整流程脚本,配套说明文档与演示视频便于快速复现与二次开发,是理解医疗类SaaS系统分层架构与真实业务闭环的优质实战范例。

1. 这不是又一个“学生毕设Demo”:Django+Vue医疗预约系统真正在跑MySQL生产级事务

你点开过多少个标着“医疗预约系统”的GitHub仓库?点进去发现只有models.py里写了三个字段,views.py里塞了段硬编码的return HttpResponse("预约成功"),前端页面连日期选择器都用<input type="date">原生控件凑数。但这个源码包不一样——它在init_sql.bat里预置了12张表的完整建表语句,在run.bat中封装了Djangorunserver与Vuenpm run serve双进程守护逻辑,在main.js.bak里甚至保留了Vue Router动态路由加载的调试痕迹。它解决的不是“能不能跑”,而是“怎么让挂号、分诊、诊断、报告回传这四个强时序环节在MySQL事务边界内不丢数据”。适合两类人:一是正被医院信息科催着交原型的Python后端工程师,二是需要把Vue组件嵌入HIS系统老界面的前端开发者。它不教你怎么写第一个Hello World,但会告诉你为什么django.db.transaction.atomic()必须包裹create_appointment()update_doctor_schedule()两个操作,以及Vue的v-model.lazy如何避免患者反复修改手机号触发高频API请求。

2. Django后端:从MySQL事务隔离到医生排班冲突检测的落地实现

2.1 为什么选Django而非Flask?核心在Admin与ORM对医疗数据模型的天然适配

医疗系统最耗时的不是业务逻辑,而是数据治理。患者主索引(EMPI)、检查项目编码(LOINC)、诊断分类(ICD-10)这些标准体系,要求后台必须能快速生成符合规范的数据录入界面。Django Admin模块直接解决了这个问题:admin.py中仅需3行代码即可暴露医生排班管理界面:

# admin.py @admin.register(DoctorSchedule) class DoctorScheduleAdmin(admin.ModelAdmin): list_display = ['doctor', 'date', 'session', 'available_slots', 'booked_slots'] list_filter = ['doctor__department', 'date', 'session'] search_fields = ['doctor__name', 'doctor__code']

提示:list_filter中嵌套doctor__department是关键——它自动将Doctor模型的department外键字段转为筛选下拉菜单,无需手写SQL JOIN。而Flask-Admin需手动定义FilterRelation类并注册,开发效率差3倍以上。

更关键的是Django ORM对ACID事务的封装能力。医疗预约本质是“锁号-扣槽-发通知”三步原子操作,任何一步失败必须回滚。models.pyAppointment模型明确声明了外键约束与非空校验:

# models.py class Appointment(models.Model): patient = models.ForeignKey(Patient, on_delete=models.PROTECT, db_index=True) doctor = models.ForeignKey(Doctor, on_delete=models.PROTECT, db_index=True) schedule = models.ForeignKey(DoctorSchedule, on_delete=models.PROTECT) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') created_at = models.DateTimeField(auto_now_add=True) class Meta: # 强制唯一性:同一医生同一时段不能重复预约 unique_together = ('doctor', 'schedule')

on_delete=models.PROTECT确保删除医生时系统抛出ProtectedError异常,而非静默级联删除——这是医疗数据合规的底线。

2.2 init_sql.bat:不是简单建表,而是构建跨科室的资源调度视图

init_sql.bat脚本远超基础建表功能。它执行的init_db.sql包含以下关键结构:

-- 创建物化视图替代方案(MySQL 5.7不支持物化视图,用存储过程模拟) DELIMITER $$ CREATE PROCEDURE RefreshDepartmentCapacity() BEGIN INSERT INTO department_capacity (dept_id, date, total_slots, booked_slots) SELECT d.id, ds.date, SUM(ds.available_slots) as total_slots, COALESCE(SUM(a.booked_count), 0) as booked_slots FROM department d JOIN doctor doc ON d.id = doc.department_id JOIN doctor_schedule ds ON doc.id = ds.doctor_id LEFT JOIN ( SELECT schedule_id, COUNT(*) as booked_count FROM appointment WHERE status IN ('confirmed', 'in_progress') GROUP BY schedule_id ) a ON ds.id = a.schedule_id GROUP BY d.id, ds.date; END$$ DELIMITER ;

该存储过程每日凌晨触发,生成department_capacity汇总表,供前端“科室余号看板”实时查询。注意COALESCE(SUM(a.booked_count), 0)——当某科室当日无预约时,SUM()返回NULL,COALESCE将其转为0,避免前端JavaScript因null + 1导致计算错误。

2.3 run.bat与2-run.bat:双进程守护中的端口冲突规避策略

run.bat直接调用python manage.py runserver 8000,而2-run.bat则执行:

@echo off REM 启动Django服务(后台) start /min python manage.py runserver 8000 REM 等待Django端口就绪(检测8000端口是否响应HTTP 200) :check_django timeout /t 2 >nul curl -s -o nul http://127.0.0.1:8000/health/ || goto check_django REM 启动Vue开发服务器 cd frontend && npm run serve

关键点在于/health/健康检查端点。views.py中新增:

# views.py from django.http import JsonResponse from django.db import connection def health_check(request): try: with connection.cursor() as cursor: cursor.execute("SELECT 1") cursor.fetchone() return JsonResponse({'status': 'ok', 'db': 'connected'}) except Exception as e: return JsonResponse({'status': 'error', 'db': str(e)}, status=503)

此端点返回JSON而非HTML,curl可精准判断服务状态。若直接ping端口,Django进程可能已启动但数据库连接未建立,导致Vue前端发起API请求时遭遇500错误。

3. Vue前端:从m3u8视频诊断回放到底层表单验证的深度集成

3.1 基于Vue CLI 4的模块化架构:为何build.bat要分三阶段执行

build.bat脚本执行顺序揭示了生产环境部署逻辑:

REM 阶段1:编译Vue静态资源 cd frontend && npm run build REM 阶段2:将dist目录复制到Django staticfiles xcopy /y /e frontend\dist\* ..\static\ REM 阶段3:收集静态文件(含Django Admin样式) python manage.py collectstatic --noinput

重点在阶段2的xcopy命令。frontend/vue.config.js中配置了publicPath: '/static/',使Vue打包后的index.html中所有资源路径前缀为/static/js/app.xxx.js。而Django的STATIC_URL = '/static/'与之完全匹配,避免了Nginx反向代理时的路径重写问题。若使用默认publicPath: '/',则需在Nginx中配置location /js/重写规则,增加运维复杂度。

3.2 诊断报告页的m3u8视频播放:Vue组件级HLS封装

医疗场景中,超声/内镜检查视频需以m3u8流式播放。ReportDetail.vue中集成hls.js

<template> <div class="video-container"> <video ref="videoPlayer" class="video-player" controls></video> <div v-if="loading" class="loading">加载中...</div> </div> </template> <script> import Hls from 'hls.js' export default { props: ['reportId'], data() { return { hls: null, loading: true, videoUrl: '' } }, async mounted() { // 1. 从API获取m3u8地址(带JWT签名防盗链) const res = await this.$http.get(`/api/reports/${this.reportId}/video-url/`) this.videoUrl = res.data.url // 2. 初始化HLS播放器 if (Hls.isSupported()) { this.hls = new Hls({ capLevelToPlayerSize: true, maxMaxBufferLength: 30, // 缓冲30秒,适应医院网络波动 enableWorker: true }) this.hls.loadSource(this.videoUrl) this.hls.attachMedia(this.$refs.videoPlayer) this.hls.on(Hls.Events.MANIFEST_PARSED, () => { this.loading = false this.$refs.videoPlayer.play() }) } else if (this.$refs.videoPlayer.canPlayType('application/vnd.apple.mpegurl')) { // Safari原生支持 this.$refs.videoPlayer.src = this.videoUrl this.$refs.videoPlayer.addEventListener('loadedmetadata', () => { this.loading = false }) } }, beforeDestroy() { if (this.hls) { this.hls.destroy() } } } </script>

注意:maxMaxBufferLength: 30参数针对医院内网带宽不稳定场景。默认值为60秒,易导致缓冲区溢出卡顿;设为30秒后,HLS自动降码率保障流畅播放。

3.3 患者预约表单:Vue与Django REST Framework的双向验证对齐

AppointmentForm.vue中,前端验证规则必须与Django Serializer严格一致,否则出现“前端提示格式正确,后端返回400”:

<template> <el-form :model="form" :rules="rules" ref="formRef"> <el-form-item label="手机号" prop="phone"> <el-input v-model="form.phone" placeholder="请输入11位手机号"></el-input> </el-form-item> <el-form-item label="预约时段" prop="schedule"> <el-select v-model="form.schedule" placeholder="请选择"> <el-option v-for="item in availableSchedules" :key="item.id" :label="item.time_range" :value="item.id" ></el-option> </el-select> </el-form-item> </el-form> </template> <script> export default { data() { return { form: { phone: '', schedule: null }, rules: { phone: [ { required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ], schedule: [ { required: true, message: '请选择预约时段', trigger: 'change' } ] } } } } </script>

对应Django的serializers.py

# serializers.py class AppointmentCreateSerializer(serializers.Serializer): phone = serializers.CharField( max_length=11, validators=[RegexValidator(r'^1[3-9]\d{9}$', '手机号格式不正确')] ) schedule = serializers.PrimaryKeyRelatedField( queryset=DoctorSchedule.objects.filter(date__gte=date.today()), error_messages={'required': '请选择预约时段'} )

关键点:前端pattern正则与后端RegexValidator完全一致,且error_messages'required'消息与前端message字段相同,保证用户看到的错误提示无歧义。

4. MySQL性能优化:从慢查询日志定位到索引失效的实战修复

4.1 诊断医生排班查询慢的根因:联合索引设计与覆盖索引应用

上线初期,/api/doctors/schedules/?date=2024-06-15&dept=cardiology接口平均响应达2.3秒。开启MySQL慢查询日志后,EXPLAIN分析结果如下:

EXPLAIN SELECT ds.id, ds.time_range, ds.available_slots, ds.booked_slots, d.name, d.title, dep.name as dept_name FROM doctor_schedule ds JOIN doctor d ON ds.doctor_id = d.id JOIN department dep ON d.department_id = dep.id WHERE ds.date = '2024-06-15' AND dep.code = 'cardiology';

结果显示type: ALL(全表扫描),key: NULL。根本原因是WHERE条件中ds.datedep.code跨两张表,MySQL无法同时使用两个单列索引。

修复方案:创建覆盖索引

-- 删除原有单列索引 DROP INDEX idx_doctor_schedule_date ON doctor_schedule; DROP INDEX idx_department_code ON department; -- 创建联合索引(按WHERE条件顺序) CREATE INDEX idx_ds_dept_date ON doctor_schedule(date, doctor_id); CREATE INDEX idx_dept_code ON department(code); -- 关键:添加冗余字段到索引,实现覆盖查询(避免回表) ALTER TABLE doctor_schedule ADD INDEX idx_ds_covering (date, doctor_id, time_range, available_slots, booked_slots);

重建索引后,EXPLAIN显示type: rangekey: idx_ds_covering,响应时间降至120ms。注意idx_ds_covering包含time_range等SELECT字段——MySQL直接从索引中读取全部数据,无需回查主键索引(B+树叶子节点),减少I/O次数。

4.2 防止幻读:Django事务中SELECT ... FOR UPDATE的精确使用

当多个护士同时为同一患者创建预约时,可能出现“超挂”(预约数超过医生当日限额)。views.py中关键逻辑:

# views.py from django.db import transaction def create_appointment(request): data = json.loads(request.body) schedule_id = data['schedule'] with transaction.atomic(): # 1. 锁定该时段记录(防止并发修改) schedule = DoctorSchedule.objects.select_for_update().get(id=schedule_id) # 2. 检查余号(此时其他事务无法修改该记录) if schedule.available_slots <= 0: raise ValidationError('该时段号源已满') # 3. 创建预约(触发外键约束检查) appointment = Appointment.objects.create( patient_id=data['patient'], doctor_id=schedule.doctor_id, schedule_id=schedule_id, status='confirmed' ) # 4. 扣减余号(原子操作) schedule.available_slots -= 1 schedule.booked_slots += 1 schedule.save() # 此save在事务内,不会被其他事务干扰 return JsonResponse({'id': appointment.id})

select_for_update()在MySQL中生成SELECT ... FOR UPDATE语句,对doctor_schedule表中该行加行锁。即使两个请求几乎同时到达,第二个请求会阻塞等待第一个事务提交,从而保证available_slots判断的准确性。若省略此行,高并发下可能出现“检查时有1个号,但两个请求同时扣减,导致-1”。

4.3 生产环境MySQL配置调优:针对医疗系统读多写少特性的参数调整

my.cnf中关键配置项(基于16GB内存服务器):

参数原值调优值说明
innodb_buffer_pool_size128M10G缓冲池占物理内存60%,容纳全部热数据(患者主索引、医生排班表)
innodb_log_file_size48M512M大事务日志减少checkpoint频率,避免预约批量导入时IO瓶颈
query_cache_type10关闭查询缓存——医疗数据实时性要求高,缓存失效成本大于收益
max_connections151500支持门诊高峰期并发连接(按每科室50护士估算)

特别注意innodb_log_file_size:若设为过小(如默认48M),当执行INSERT INTO appointment ... SELECT批量导入历史预约时,InnoDB频繁刷脏页,导致SHOW PROCESSLIST中大量Writing to log状态。512M值经压测验证,可支撑单次导入5万条预约记录无明显延迟。

5. 宝塔面板部署实操:从Python环境隔离到Nginx反向代理的完整链路

5.1 Python环境隔离:为什么必须用venv而非系统Python

1-install.bat中执行:

python -m venv venv venv\Scripts\activate.bat pip install -r requirements.txt

提示:宝塔面板中若直接在系统Python(如/usr/bin/python3)安装Django,会导致django-admin命令与系统其他Python项目冲突。venv创建独立环境,pip list仅显示本项目依赖,pip freeze > requirements.txt输出纯净依赖列表。

在宝塔中创建站点时,Python版本选择“纯静态”,然后在“网站设置→配置文件”中修改:

# 宝塔Nginx配置片段 location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源直通(绕过Django) location /static/ { alias /www/wwwroot/medical/static/; expires 1y; add_header Cache-Control "public, immutable"; }

关键点:proxy_pass指向Django开发服务器(生产环境应替换为gunicorn),而/static/路径由Nginx直接提供,避免Django处理静态文件的性能损耗。

5.2 Vue路由history模式下的404问题:Nginx重写规则详解

frontend/vue.config.js中启用history模式:

module.exports = { publicPath: '/', devServer: { historyApiFallback: true } }

此模式下,访问/patient/123时浏览器不发送请求,但刷新页面会向服务器请求/patient/123路径。若Nginx未配置重写,返回404。在宝塔站点配置中添加:

location / { try_files $uri $uri/ /index.html; }

try_files指令含义:先尝试匹配$uri(如/static/css/app.css),再匹配$uri/(目录),最后全部fallback到/index.html(Vue Router接管)。此规则确保Vue Router的router.push('/report/456')在刷新时仍能正确加载。

5.3 数据库连接安全:MySQL远程访问的最小权限授予

在宝塔MySQL管理中,不创建'medical'@'%'用户,而是精确授权:

-- 创建专用用户(密码强度要求:大小写字母+数字+符号,长度≥12) CREATE USER 'medical_app'@'localhost' IDENTIFIED BY 'Med@2024!SecurePass'; -- 仅授予必要权限(禁止DROP、CREATE等高危操作) GRANT SELECT, INSERT, UPDATE, DELETE ON medical_db.* TO 'medical_app'@'localhost'; -- 刷新权限 FLUSH PRIVILEGES;

Django的settings.py中配置:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'HOST': '127.0.0.1', # 强制走本地socket,禁用TCP/IP 'PORT': '3306', 'NAME': 'medical_db', 'USER': 'medical_app', 'PASSWORD': os.environ.get('DB_PASSWORD'), # 密码从环境变量读取 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", } } }

HOST: '127.0.0.1'强制MySQL走TCP连接(而非Unix socket),配合'medical_app'@'localhost'用户,确保连接来源可控。sql_mode='STRICT_TRANS_TABLES'开启严格模式,避免INSERT INTO appointment (phone) VALUES ('abc')插入空字符串导致数据污染。

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

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

芯片工艺描述:从后端参数到设计主轴的范式升级

1. 项目概述&#xff1a;为什么“芯片类型描述工艺”是设计阶段不可绕过的硬门槛最近在好几个芯片设计项目的早期评审会上&#xff0c;我都听到同一个问题被反复抛出来&#xff1a;“这个模块用的是什么工艺节点&#xff1f;是FinFET还是FD-SOI&#xff1f;衬底类型是体硅还是S…

作者头像 李华
网站建设 2026/9/14 2:14:56

Python旅游景点爬虫实战:去哪网景点数据采集与源码设计解析

简介&#xff1a;这是一套基于Python开发的去哪网旅游景点爬虫源码项目&#xff0c;适合Python爬虫初学者及旅游数据分析者。项目以去哪网为目标站点&#xff0c;通过get.py与app.py两个核心脚本完成景点信息的抓取、解析与整理&#xff0c;并输出为结构化表格数据&#xff0c;…

作者头像 李华
网站建设 2026/9/14 2:14:55

Claude Code 配 TaoToken:从源码看 AI Agent 的权限与上下文设计

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

作者头像 李华
网站建设 2026/9/14 2:14:22

嵌入式软件架构设计:分层事件驱动(LEDA)实战指南

1. 为什么“堆代码”是嵌入式开发最隐蔽的慢性毒药&#xff1f;你有没有过这样的经历&#xff1a;一个STM32项目&#xff0c;初期功能跑通很快&#xff0c;LED亮了、串口吐数据了、ADC采样也准——但三个月后&#xff0c;新加一个CAN总线收发功能&#xff0c;改了三处头文件&am…

作者头像 李华
网站建设 2026/9/14 2:14:04

毕业论文AI生成靠谱吗?这些坑提前知道

打开电脑&#xff0c;看到导师发来的消息&#xff1a;"初稿周三前给我看看。"再看看文档里那个写了三天的标题&#xff0c;这种感觉我太熟悉了。于是很多人把目光投向了AI——毕业论文AI生成靠谱吗&#xff1f;说实话&#xff0c;工具本身没毛病&#xff0c;但用法不…

作者头像 李华