简介:本资源是一套基于Python全栈技术的医疗预约与诊断系统完整开发包,面向Web开发初学者及医疗信息化实践者,解决传统线下挂号流程长、医患信息不同步、诊疗数据难追溯等痛点。系统采用Django构建稳健后端(含53个核心py文件与1个初始化SQL脚本),Vue实现响应式前端(147个vue组件+63个js逻辑文件+15个css样式文件),MySQL存储患者档案、预约记录、诊断结果等关键业务数据,辅以HTML/CSS/JS完成界面渲染,整体结构清晰、模块解耦度高。压缩包共689个文件,涵盖159个SVG图标、101个JPG/PNG素材、1个MP4演示视频、多个bat一键部署脚本(如init_sql.bat、run.bat)及说明文档,总大小105.17MB。目前已有83人学习下载,读者可直接运行调试、理解前后端分离架构落地细节,掌握医疗场景下的用户权限控制、实时预约状态更新、检查报告动态展示等典型功能实现方案。
1. 这不是又一个“前后端分离模板”,而是一套能跑通挂号、分诊、电子病历与医生接诊闭环的 Django + Vue 医疗系统
你在网上搜“Django vue 医疗系统”,大概率会看到一堆空壳项目:前端只有登录页,后端 API 返回{"code": 0, "data": []},数据库 schema 缺失字段,requirements.txt里写着Django==2.2—— 而真实医疗场景要处理的是:患者实名认证后预约指定科室/医生/时段,候诊队列自动排序,医生端实时查看当前叫号+历史诊断记录,上传检查报告(PDF/影像缩略图),生成结构化电子病历(ICD-10 编码、主诉、现病史、诊断结论字段可编辑),且所有操作需符合《电子病历系统功能应用水平分级评价标准》中对操作留痕、数据不可篡改的基本要求。本项目源码包(.zip)正是围绕这些刚性需求构建:Django 4.2 提供强校验的 RESTful 接口与 admin 后台,Vue 3 + Composition API 实现响应式预约看板与诊断表单,MySQL 存储带外键约束的患者-医生-预约-病历四维关系模型。适合正在落地基层医院信息化、互联网诊疗平台 MVP 或高校课程设计的学生团队——它不教你怎么写 Hello World,而是告诉你:当患者点击“预约张主任周三上午”时,后端如何锁住该时段、校验医生排班、生成唯一预约号并推送短信;当医生在 Vue 页面勾选“高血压3级 很高危”,系统如何自动关联 ICD-10 编码I10并写入病历结构体。
2. 用 Django 4.2 构建医疗领域强约束后端:从模型设计到 REST API 安全暴露
医疗数据敏感性决定了后端不能只做 CRUD。本系统在 Django 层做了三层硬性控制:模型层强制字段约束、视图层权限隔离、API 层操作审计。下面拆解关键实现路径。
2.1 医疗核心模型设计:用models.py定义可审计的实体关系
医疗业务的本质是状态流转,因此模型必须体现“不可逆”与“可追溯”。例如Appointment(预约)模型不直接存status字符串,而是用IntegerChoices枚举:
# models.py class Appointment(models.Model): class Status(models.IntegerChoices): PENDING = 0, '待确认' CONFIRMED = 1, '已确认' CHECKED_IN = 2, '已签到' IN_PROGRESS = 3, '诊疗中' COMPLETED = 4, '已完成' CANCELLED = -1, '已取消' patient = models.ForeignKey('Patient', on_delete=models.PROTECT) doctor = models.ForeignKey('Doctor', on_delete=models.PROTECT) schedule_slot = models.ForeignKey('ScheduleSlot', on_delete=models.PROTECT) status = models.SmallIntegerField(choices=Status.choices, default=Status.PENDING) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # 关键:每次状态变更必须记录操作人(医生或管理员) last_updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True )提示:
on_delete=models.PROTECT防止误删医生/患者导致预约数据孤儿;last_updated_by字段配合 Django 自带的LogEntry可追溯谁在何时将预约从PENDING改为CONFIRMED。
再看MedicalRecord(电子病历)模型,它不是自由文本字段,而是结构化 JSONField(Django 4.2 原生支持):
class MedicalRecord(models.Model): appointment = models.OneToOneField(Appointment, on_delete=models.CASCADE) # 主诉、现病史等字段独立存储,便于后续 NLP 分析或报表统计 chief_complaint = models.TextField(max_length=500, blank=True) history_of_present_illness = models.TextField(blank=True) diagnosis_icd10 = models.CharField(max_length=10, blank=True) # 如 I10 diagnosis_text = models.CharField(max_length=200, blank=True) # 如 "原发性高血压" # 检查报告附件:关联多个 FileField(实际存于 media/records/ 下) attachments = models.ManyToManyField('Attachment', blank=True)2.2 用 Django REST Framework 构建带业务规则的 API
单纯序列化模型不够。例如预约创建接口/api/appointments/必须校验:该时段是否已被约满、患者当日是否已预约同科室、医生是否处于停诊状态。这些逻辑写在serializers.py的create()方法中:
# serializers.py class AppointmentCreateSerializer(serializers.ModelSerializer): class Meta: model = Appointment fields = ['patient', 'doctor', 'schedule_slot'] def create(self, validated_data): slot = validated_data['schedule_slot'] # 校验时段是否超限(每个时段最多3个预约) if Appointment.objects.filter( schedule_slot=slot, status__in=[Appointment.Status.PENDING, Appointment.Status.CONFIRMED] ).count() >= 3: raise serializers.ValidationError("该时段预约已满") # 校验患者当日同科室预约数(防黄牛) today = timezone.now().date() same_dept_count = Appointment.objects.filter( patient=validated_data['patient'], doctor__department=validated_data['doctor'].department, schedule_slot__date=today, status__in=[Appointment.Status.PENDING, Appointment.Status.CONFIRMED] ).count() if same_dept_count >= 2: raise serializers.ValidationError("同一科室每日最多预约2次") # 创建预约并设置初始状态 instance = super().create(validated_data) instance.status = Appointment.Status.PENDING instance.save() return instance2.3 权限与审计:用 Django Admin 和自定义中间件加固生产环境
医疗系统必须满足“谁操作、何时操作、操作什么”的审计要求。本项目启用 Django 内置django.contrib.admin并做两处增强:
- Admin 界面定制:在
admin.py中隐藏敏感字段(如患者身份证号明文),仅显示脱敏后字符串:
# admin.py @admin.register(Patient) class PatientAdmin(admin.ModelAdmin): list_display = ['name', 'gender', 'age', 'id_card_masked', 'created_at'] readonly_fields = ['id_card_masked'] # 只读显示 def id_card_masked(self, obj): if obj.id_card: return obj.id_card[:6] + '*' * 8 + obj.id_card[-4:] return '' id_card_masked.short_description = '身份证号(脱敏)'- 操作日志中间件:在
middleware.py中记录关键 API 请求:
# middleware.py class AuditMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if request.path.startswith('/api/appointments/') and request.method == 'POST': # 记录预约创建行为 if request.user.is_authenticated: AuditLog.objects.create( user=request.user, action='CREATE_APPOINTMENT', target_id=request.POST.get('schedule_slot'), ip_address=get_client_ip(request) ) return self.get_response(request)| 参数 | 说明 | 本项目取值 |
|---|---|---|
DEBUG | 开发模式开关 | False(生产环境强制关闭) |
SECRET_KEY | 加密密钥 | 从.env文件读取,不提交 Git |
DATABASE_URL | MySQL 连接串 | mysql://user:pass@localhost:3306/medical_db |
ALLOWED_HOSTS | 允许访问的域名 | ['hospital.example.com', 'www.hospital.example.com'] |
3. 用 Vue 3 + Pinia 实现高交互医疗前端:从预约看板到结构化病历编辑
Vue 端不是静态页面堆砌,而是围绕医生和患者两类角色构建状态驱动 UI。核心挑战在于:如何让医生在 3 秒内完成一次完整接诊?答案是状态机驱动 + 组件复用。
3.1 路由与权限守卫:按角色加载不同布局
系统使用 Vue Router 4 的meta字段标记路由权限,并在router/index.js中统一守卫:
// router/index.js const routes = [ { path: '/doctor/dashboard', name: 'DoctorDashboard', component: () => import('@/views/doctor/Dashboard.vue'), meta: { requiresAuth: true, role: 'doctor' } }, { path: '/patient/appointments', name: 'PatientAppointments', component: () => import('@/views/patient/Appointments.vue'), meta: { requiresAuth: true, role: 'patient' } } ] router.beforeEach((to, from, next) => { const user = useUserStore() if (to.meta.requiresAuth && !user.isAuthenticated) { next({ name: 'Login' }) } else if (to.meta.role && user.role !== to.meta.role) { next({ name: 'Forbidden' }) // 403 页面 } else { next() } })3.2 预约看板组件:用 Composition API 实现实时队列刷新
医生端首页(Dashboard.vue)需展示“当前叫号”、“待接诊列表”、“今日已完成”。为避免轮询浪费资源,采用 EventSource(SSE)监听后端推送:
<!-- views/doctor/Dashboard.vue --> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import { useAppointmentStore } from '@/stores/appointment' const appointmentStore = useAppointmentStore() const currentCall = ref(null) const pendingList = ref([]) onMounted(() => { // 建立 SSE 连接,监听 /api/sse/doctor/{id}/queue const eventSource = new EventSource(`/api/sse/doctor/${appointmentStore.doctorId}/queue`) eventSource.onmessage = (e) => { const data = JSON.parse(e.data) if (data.type === 'CURRENT_CALL') { currentCall.value = data.payload } else if (data.type === 'PENDING_UPDATE') { pendingList.value = data.payload } } // 组件卸载时关闭连接 onUnmounted(() => { eventSource.close() }) }) </script>注意:Django 后端需提供
/api/sse/doctor/{id}/queue视图,使用StreamingHttpResponse持续发送 JSON 数据,每条消息以data: {...}\n\n格式分隔。这比 WebSocket 更轻量,且天然支持浏览器自动重连。
3.3 结构化病历编辑器:用 JSON Schema 动态渲染表单
电子病历字段非固定,需支持不同科室扩展。本项目采用@json-editor/json-editor库,根据后端返回的 Schema 动态生成表单:
// composable/useMedicalRecord.js export function useMedicalRecord() { const schema = ref({ type: 'object', properties: { chief_complaint: { type: 'string', title: '主诉', maxLength: 500 }, diagnosis_icd10: { type: 'string', title: 'ICD-10 编码', enum: ['I10', 'E11.9', 'J45.909'], // 从 API 获取动态枚举 enumNames: ['原发性高血压', '2型糖尿病', '未特指的哮喘'] } } }) const formData = ref({}) const saveRecord = async () => { try { await api.post('/api/medical-records/', { appointment_id: route.params.id, ...formData.value }) ElMessage.success('病历保存成功') } catch (err) { ElMessage.error('保存失败:' + err.response?.data?.detail) } } return { schema, formData, saveRecord } }4. 在 Linux 服务器上部署 Django + Vue 医疗系统:Nginx + Gunicorn + MySQL 生产配置
本地开发能跑通不等于生产可用。医疗系统对稳定性、HTTPS、并发承载有硬性要求。本节给出宝塔面板用户可直接复用的部署方案(也兼容纯命令行)。
4.1 后端部署:Gunicorn + Supervisor 管理 Django 进程
Django 官方明确不推荐用runserver上线。本项目使用 Gunicorn 作为 WSGI 服务器,配置文件gunicorn.conf.py:
# gunicorn.conf.py import multiprocessing bind = '127.0.0.1:8000' bind_address = '127.0.0.1:8000' workers = multiprocessing.cpu_count() * 2 + 1 worker_class = 'sync' worker_connections = 1000 max_requests = 1000 max_requests_jitter = 100 timeout = 30 keepalive = 2 preload = True daemon = False raw_env = ['DJANGO_SETTINGS_MODULE=medical.settings.production']启动命令(需先激活虚拟环境):
# 安装 gunicorn pip install gunicorn # 启动(后台运行) gunicorn -c gunicorn.conf.py medical.wsgi:application # 或用 supervisor 管理(推荐) echo "[program:django] command=/opt/venv/bin/gunicorn -c /var/www/medical/gunicorn.conf.py medical.wsgi:application directory=/var/www/medical user=www-data autostart=true autorestart=true redirect_stderr=true stdout_logfile=/var/log/medical/django.log" > /etc/supervisor/conf.d/medical.conf supervisorctl reread supervisorctl update supervisorctl start django4.2 前端部署:Vue CLI 打包后由 Nginx 静态托管
Vue 项目执行npm run build生成dist/目录,关键配置在vue.config.js中指定反向代理路径:
// vue.config.js module.exports = { outputDir: 'dist', publicPath: '/static/', // 与 Django 的 STATIC_URL 一致 devServer: { proxy: { '/api': { target: 'http://127.0.0.1:8000', // 指向 Django 后端 changeOrigin: true } } } }Nginx 配置(/www/wwwroot/hospital.example.com/conf/nginx.conf):
server { listen 80; server_name hospital.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name hospital.example.com; ssl_certificate /www/server/panel/vhost/cert/hospital.example.com/fullchain.pem; ssl_certificate_key /www/server/panel/vhost/cert/hospital.example.com/privkey.pem; location / { root /www/wwwroot/hospital.example.com/dist; try_files $uri $uri/ /index.html; } location /api/ { 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; } location /media/ { alias /var/www/medical/media/; expires 1h; add_header Cache-Control "public, immutable"; } }提示:
/media/路径必须指向 Django 的MEDIA_ROOT,确保患者上传的检查报告 PDF 能被正确访问;add_header Cache-Control对静态资源启用缓存,降低带宽压力。
4.3 数据库与安全加固:MySQL 隔离与 Django 密码策略
医疗数据存储必须满足最小权限原则。创建专用数据库用户:
-- 登录 MySQL CREATE DATABASE medical_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'medical_app'@'localhost' IDENTIFIED BY 'StrongPassw0rd!'; GRANT SELECT, INSERT, UPDATE, DELETE ON medical_db.* TO 'medical_app'@'localhost'; FLUSH PRIVILEGES;Django 设置中启用密码强度校验(settings.py):
AUTH_PASSWORD_VALIDATORS = [ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 10}}, {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, # 自定义:禁止使用身份证号后6位 {'NAME': 'medical.validators.NoIDCardTailValidator'}, ]自定义校验器示例:
# validators.py from django.core.exceptions import ValidationError from django.contrib.auth.password_validation import CommonPasswordValidator def NoIDCardTailValidator(password, user=None): if user and user.patient_profile and user.patient_profile.id_card: tail = user.patient_profile.id_card[-6:] if tail in password: raise ValidationError( "密码不能包含身份证号后6位", code='id_card_tail_in_password' )5. 验证医疗系统合规性的三个关键动作:数据一致性检查、操作留痕回溯、HTTPS 强制跳转
部署完成后,不能只测“按钮能不能点”,而要验证是否满足医疗信息化基本合规要求。以下是上线前必须执行的三项技术验证。
5.1 用 Django Shell 检查核心数据链路完整性
进入 Django shell,模拟一次完整预约-接诊-归档流程,验证外键约束与状态流转:
# python manage.py shell >>> from medical.models import Patient, Doctor, ScheduleSlot, Appointment, MedicalRecord >>> # 1. 创建测试患者 >>> p = Patient.objects.create(name="张三", id_card="11010119900307251X", phone="13800138000") >>> # 2. 获取今日张主任的首个可约时段 >>> slot = ScheduleSlot.objects.filter( ... doctor__name="张主任", ... date=date.today(), ... status=ScheduleSlot.Status.AVAILABLE ... ).first() >>> # 3. 创建预约 >>> appt = Appointment.objects.create(patient=p, doctor=slot.doctor, schedule_slot=slot) >>> appt.status 0 # PENDING >>> # 4. 医生接诊并生成病历 >>> appt.status = Appointment.Status.IN_PROGRESS >>> appt.save() >>> mr = MedicalRecord.objects.create( ... appointment=appt, ... chief_complaint="头晕3天", ... diagnosis_icd10="I10", ... diagnosis_text="原发性高血压" ... ) >>> # 5. 检查是否生成了审计日志 >>> from django.contrib.admin.models import LogEntry >>> LogEntry.objects.filter(object_id=appt.id).count() 3 # 创建预约、修改状态、创建病历各1条5.2 用 curl 验证 HTTPS 强制跳转与 API 权限隔离
医疗系统严禁 HTTP 明文传输。用命令行验证:
# 测试 HTTP 是否自动跳转 HTTPS curl -I http://hospital.example.com/api/appointments/ # 应返回:HTTP/1.1 301 Moved Permanently # Location: https://hospital.example.com/api/appointments/ # 测试未登录访问 API curl -X GET https://hospital.example.com/api/appointments/ # 应返回:HTTP/1.1 401 Unauthorized # {"detail":"身份认证信息未提供。"} # 测试患者 token 访问医生专属接口 TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." curl -H "Authorization: Bearer $TOKEN" \ https://hospital.example.com/api/doctor/dashboard/ # 应返回:HTTP/1.1 403 Forbidden # {"detail":"您没有执行此操作的权限。"}5.3 用 Chrome DevTools 检查前端敏感信息泄露风险
打开医生端页面,执行以下检查:
- Network Tab:筛选
XHR,查看所有请求 URL 是否含?token=或?secret=参数(应全部通过Authorization: Bearer xxx传递); - Application Tab → Local Storage:确认无
patient_id_card、doctor_license_no等明文存储; - Console Tab:执行
console.log(JSON.stringify(window.__INITIAL_STATE__)),检查初始化数据是否脱敏(如身份证号应为110101******251X); - Elements Tab:右键任意患者姓名,检查 DOM 中是否存在
>configureWebpack: { optimization: { minimize: true, splitChunks: { chunks: 'all' } } }本文还有配套的精品资源,点击获取