简介:本资源是一套基于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.py中Appointment模型明确声明了外键约束与非空校验:
# 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.date与dep.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: range,key: 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_size | 128M | 10G | 缓冲池占物理内存60%,容纳全部热数据(患者主索引、医生排班表) |
innodb_log_file_size | 48M | 512M | 大事务日志减少checkpoint频率,避免预约批量导入时IO瓶颈 |
query_cache_type | 1 | 0 | 关闭查询缓存——医疗数据实时性要求高,缓存失效成本大于收益 |
max_connections | 151 | 500 | 支持门诊高峰期并发连接(按每科室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')插入空字符串导致数据污染。
本文还有配套的精品资源,点击获取