Python Django实战:从零构建医院信息管理系统

发布时间:2026/10/2 15:15:21
Python Django实战:从零构建医院信息管理系统 各位做Python后端的朋友应该都有过这种体会网上的电商项目、博客系统、管理系统遍地都是但真正贴近医疗业务、能让你摸到“业务复杂度”的项目范例反而很少。我前前后后做过几个信息管理类的系统这次花了大概三周业余时间用Python和Django完整做了一套医院信息管理系统HIS。从患者建档、门诊挂号、医生看诊、开立处方到药房发药核心业务闭环全部跑通权限控制和操作日志也做了比较细的划分。这篇文章就把这套系统的设计与实现全过程拆开讲清楚包括表结构怎么设计、权限怎么落地、哪些坑一定绕开以及上线部署时会遇到的真实问题。适合正在学Django想找个完整项目练手的人也适合要接手医疗信息管理系统开发的同学参考。1. 项目整体设计与技术选型思路1.1 为什么在这个场景选Python与Django医院信息管理系统和普通的增删改查系统最大的区别是业务流程长、角色多、数据敏感、逻辑状态复杂。拿挂号来说一个挂号动作牵扯到患者档案、科室排班、号源余量、费用记录患者看完病病历、诊断、处方、检查检验结果又要串联起来。这时候开发框架的“规矩感”就特别重要。选择Django而不选Flask核心原因有三个。第一Django自带Admin后台和ORM能极大提升管理端的开发效率。医院系统里像科室管理、药品目录维护、排班调整这类低频但强依赖的功能用Django自带后台稍作定制就能交付不用额外写一堆管理页面。第二Django的认证认证和权限系统非常成熟User、Group、Permission这套机制能直接支撑医生、护士、药师、收费员、管理员等多角色的权限隔离。第三Django的ORM在复杂查询和事务处理上能力扎实比如预约挂号、收费结算这种强一致性的操作用transaction.atomic()可以轻松保证数据不出现“扣费了号却没挂上”这种问题。我做这套系统时不追求花哨的前后端分离架构而是采用服务端渲染加少量AJAX的经典方案。为什么这么选因为医院信息管理系统的主要使用场景是院内局域网使用者是医生、护士、收费员这类固定人员交互核心是表单填写和数据检索服务端渲染完全够用而且开发和维护成本低很多。如果硬上VueDRFDjango REST Framework开发周期至少翻一倍对这类项目来说性价比并不高。1.2 信息系统的模块边界与角色划分动手写代码之前必须先明确系统要管什么、谁在用、每个角色能碰哪些数据。我最终把系统切成了这几个核心模块患者管理、科室与医生排班、门诊挂号、医生工作站病历与诊断、处方管理、收费管理、药房发药、系统管理。角色划分为四类比较合适角色核心职责典型操作管理员基础数据维护与账号分配维护科室、药品、医生账号、角色权限收费员/挂号员患者建档与挂号收费新增患者、挂号和退号、费用结算医生看诊与病历记录写病历、下诊断、开处方、开检验检查单药师发药核验审核处方、确认发药、库存扣减这个划分几乎决定了后续所有的数据表设计和路由分组。比如药品目录只允许管理员维护医生只能查询和选择处方用药药师只能看到待发药的处方。这些约束不是靠“约定”去保证的而是靠Django的权限装饰器、视图基类和模板层判断一层层卡死的。所以第3章我会专门说权限落地的细节这里先有个整体印象即可。2. 数据库设计与核心数据模型2.1 患者、医生、科室三个基础实体的建模数据库设计是整个系统里最不能急的环节。医院系统数据有一个特点同一张表要同时满足业务查询和统计报表两类需求。比如患者表既要支持挂号时按姓名或病历号快速检索又要支持后续的疾病统计、就诊次数分析。所以字段设计要预留足够的统计维度。患者表我采用的字段方案是姓名、性别、出生日期、身份证号、手机号、紧急联系人、血型、过敏史、首诊日期、病历号。其中病历号是全局唯一的业务主键用uuid 日期规则生成格式类似20240513-0001方便门诊窗口快速录入。身份证号做了唯一约束但允许空值因为一部分婴幼儿患者没有身份证。还需要说明的是敏感字段如身份证号、手机号在数据库中建议使用加密存储或脱敏展示这个我在第4章展开讲。医生表和科室表之间存在明确归属关系医生属于某个科室但一个医生也可能兼多个科室的出诊。所以医生表设计为class Department(models.Model): name models.CharField(max_length64, uniqueTrue, verbose_name科室名称) code models.CharField(max_length32, uniqueTrue, verbose_name科室编码) parent models.ForeignKey(self, nullTrue, blankTrue, verbose_name上级科室, on_deletemodels.PROTECT) sort_order models.IntegerField(default0, verbose_name排序) is_active models.BooleanField(defaultTrue, verbose_name是否启用) class Doctor(models.Model): user models.OneToOneField(User, on_deletemodels.PROTECT, verbose_name登录账号) real_name models.CharField(max_length32, verbose_name真实姓名) title models.CharField(max_length32, verbose_name职称) departments models.ManyToManyField(Department, verbose_name出诊科室) introduction models.TextField(blankTrue, verbose_name医生简介)为什么用OneToOneField连接Django的User表而不是自己建一套登录表因为认证、密码盐值、会话管理这些底层逻辑Django已经做得很稳重复造轮子反而容易出安全问题。医生表只放医疗业务属性登录凭据交给User表统一管理各司其职。2.2 挂号、病历、处方、收费的业务表设计挂号表是整个流程的主线。它的设计直接影响后续病历和收费的关联关系。核心字段包括号源时间、科室、医生、患者、挂号状态、费用状态、挂号类型普通号/专家号、费用金额。这里必须把费用状态和就诊状态拆开因为现实中会出现“付了费但医生临时停诊”“看完病还没缴费”这类状态交叉的情况。class Registration(models.Model): STATUS_CHOICES [(pending, 待就诊), (finished, 已就诊), (cancelled, 已取消)] PAY_STATUS [(unpaid, 未缴费), (paid, 已缴费), (refunded, 已退款)] patient models.ForeignKey(Patient, on_deletemodels.PROTECT, verbose_name患者) doctor models.ForeignKey(Doctor, on_deletemodels.PROTECT, verbose_name医生) department models.ForeignKey(Department, on_deletemodels.PROTECT, verbose_name科室) regist_date models.DateField(auto_now_addTrue, verbose_name挂号日期) visit_time models.DateTimeField(verbose_name号源时间) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultpending) pay_status models.CharField(max_length16, choicesPAY_STATUS, defaultunpaid) fee models.DecimalField(max_digits10, decimal_places2, verbose_name挂号费用) operator models.ForeignKey(User, on_deletemodels.PROTECT, verbose_name经办人)病历表与挂号一对一关联处方与挂号一对多关联。病历记录诊察信息、主诉、现病史、初步诊断处方记录一组药品和用法用量。这里需要纠正很多新人容易犯的错误不要把药品直接塞进处方主表。正确的做法是拆出处方主表和处方明细表两张表主表记录医生、时间、总金额、状态明细表记录每一个药品的单价、数量、用法。这样的好处是一张处方可以包含口服药和注射药后续退药时也只需要针对明细项操作。2.3 设计过程中容易踩的几个坑第一坑全表都用自增ID。医院系统数据是要长期沉淀的自增ID在跨系统迁移、合并数据时会产生冲突。我的做法是业务主键用UUID或带规则的字符串自增ID只作为内部物理键使用。这一点在前期不重视后期做数据迁移时绝对想哭。第二坑外键全部用CASCADE级联删除。医疗数据的删除必须受控。患者挂了号、医生写了病历就不能直接物理删除这些数据涉及医疗记录保留。所以关键业务表的外键一律用PROTECT删除操作交给业务层的固定流程退号、作废、软删去完成。对外提供服务时只有管理员可以做有限的数据清理。第三坑时间字段不够用。系统既要支持“某医生某天上午有没有号”又要支持“最近三个月门诊量统计”。所以挂号表的时间字段我建议拆成date和time两种粒度分开存不要全部用DateTimeField。按日期检索走date字段走索引按具体时刻展示用time字段查询效率差异非常明显。3. 核心业务实现从注册登录到权限控制3.1 基于Django的用户模型与扩展表认证这块我直接用Django内置的User模型并开启AUTH_USER_MODEL扩展。为什么不用自定义用户表因为内置User已经包含了密码管理、会话管理、管理员标记等完整能力扩展成本最低。需要增加手机号、职称这类字段时通过OneToOne的方式关联到Profile或直接交给Doctor表。登录方式上系统支持用户名密码。很多医院实际场景是给每个医生建一个工号账号医疗系统不太适合开放注册所以账号由管理员统一创建并分配初始密码首次登录强制改密。这样的做法在真实项目中非常实用能避免出现大量免密或弱密码账号的隐患。3.2 权限控制的具体落地权限这块要分层看。第一层是菜单权限即不同角色登录后能看到不同的导航菜单。第二层是操作权限比如收费员只能写挂号单和收费单不能改药品目录。第三层是数据权限医生只能看自己的病历和处方护士能看本科室的数据这已经属于行级权限概念。Django自带的login_required和permission_required装饰器能解决前两层。我在具体实践时做了一个简洁的权限标记方式from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import Permission # 在创建初始数据时为系统预置角色权限 def setup_permissions(): reg_perm Permission.objects.get(codenameadd_registration) dispense_perm Permission.objects.get(codenamedispense_prescription) doctor_group Group.objects.get(name医生) doctor_group.permissions.add(reg_perm, dispense_perm)然后在每个视图上明确标注login_required permission_required(his.add_registration) def create_registration(request): # 挂号业务逻辑但关于数据权限Django内置机制没有现成方案需要在查询时主动过滤。我的习惯是封装一个服务层函数def get_visible_registrations(user): qs Registration.objects.all() if user.role doctor: qs qs.filter(doctor__useruser) elif user.role nurse: qs qs.filter(department__inuser.staff.departments.all()) return qs.select_related(patient, doctor, department)所有视图都调用这个服务层获取可见数据集而不是直接在视图里写objects.all()。这个习惯一旦养成了后面做数据安全和合规审计会省掉大量返工。3.3 完整业务流程挂号、看诊、开药、收费我把最核心的“门诊闭环”走了一遍方便你对照理解整个系统的数据流。患者建档与挂号患者到窗口收费员检索建档信息如果没有档案则填写基础信息并分配病历号建档后选择科室、医生、号源时间段系统校验号源余量并生成挂号单。这里要注意号源扣减和挂号单生成必须放在一个数据库事务里否则高并发时会出现“超卖”。核心代码片段from django.db import transaction from django.core.exceptions import ValidationError transaction.atomic def create_registration(patient_id, doctor_id, visit_time, operator): doctor Doctor.objects.select_for_update().get(iddoctor_id) if doctor.remaining_slots 0: raise ValidationError(当前医生号源已约满) registration Registration.objects.create(...) doctor.remaining_slots - 1 doctor.save() return registration这里用了select_for_update()它的作用是给医生排班记录加行锁防止两个窗口同时挂号导致号源扣重。这是本项目里一个非常关键的技术点新人不注意就会踩并发坑。医生看诊写病历患者到诊室医生调出挂号单选择“开始看诊”系统中挂号状态从待就诊变更为已就诊。接着医生填写病历主诉、现病史、诊断并开具处方。处方提交后系统自动根据药品目录计算金额生成待缴费处方单。收费与发药患者到收费窗口缴费收费员确认收费后处方状态变为已缴费同时药房端出现待发药任务。药师核对药品和处方后确认发药库存扣减并生成发药记录。这个流程看起来简单实际是很多细节堆出来的。核心原则就是一个——状态流转必须显式记录不许用“猜”的方式判断当前走到哪一步。我的做法是为核心业务对象增加一个status_logJSON字段或单独建状态变迁表每一步操作都追加一条日志。后面做系统排查时这个设计能救命。4. 安全合规与数据保护的实践细节4.1 医疗数据合规的最低要求医疗行业数据敏感度极高即使做一个项目练手我也建议从第一天开始就按生产级标准要求自己养成好习惯。这里说几个最低层面的实践。传输加密生产环境必须开启HTTPSDjango侧需要设置SECURE_SSL_REDIRECT True和SESSION_COOKIE_SECURE True。如果项目在局域网内跑可能没有正式证书那就用自签名证书加SECURE_PROXY_SSL_HEADER至少保证数据不是明文裸奔。敏感字段加密身份证号、手机号这类字段不要明文入库。简单的做法是使用Django内置的signing模块进行对称加密保存读取时解密from django.core import signing class Patient(models.Model): encrypted_id_card models.TextField(verbose_name身份证号密文) property def id_card(self): return signing.loads(self.encrypted_id_card) id_card.setter def id_card(self, value): self.encrypted_id_card signing.dumps(value)这样既不影响业务正常使用又避免了数据库泄露导致的大规模敏感信息外流。操作日志关键写入操作包括挂号、处方修改、退号、发药、权限变更都要记录操作人、操作时间、操作内容。Django有个简单有效的方案是重写Model.save()或在视图层统一记录。我实际采用的是后者在业务服务层函数中统一埋点比如挂号成功后主动写入一条OperationLog记录。4.2 日志审计与敏感字段脱敏日志除了要记录“谁在什么时间干了什么”还要考虑展示时的脱敏处理。比如在查询列表页展示患者信息时手机号只显示前三位和后四位身份证号只显示出生年月日部分。这个可以在模型层面做一个脱敏属性也可以在模板层用过滤器。我更推荐前者因为调接口、导出Excel时也能复用同一套脱敏逻辑。class Patient(models.Model): property def masked_phone(self): if not self.phone: return return self.phone[:3] **** self.phone[-4:]这套系统的权限控制设计还有一个细节值得专门强调管理员账号尽量不要直接查看患者敏感信息而是受控的“查看申请-审计”模式。但考虑到中小型医院信息系统的实际使用习惯这个尺度可以根据需求调整。我的做法是把脱敏规则做成配置项敏感字段的完整展示需要单独的高级权限这样可以兼顾日常使用和数据安全。5. 性能优化与常见问题排查5.1 ORM的N1查询陷阱Django ORM写起来方便但新手很容易写出性能灾难代码。典型场景医生工作站需要展示患者列表和最近一次挂号记录如果直接循环拿数据就会引起N1查询表格每多一行就多一次数据库交互。解决方式很直接用select_related()解决外键查询用prefetch_related()解决多对多和反向关系查询# 糟糕的写法 patients Patient.objects.all() for p in patients: registrations p.registration_set.all() # 每循环一次查一次库 # 推荐的写法 patients Patient.objects.prefetch_related(registration_set).all() for p in patients: registrations p.registration_set.all() # 预查询已经完成这个优化在数据量小于100条时感觉不明显一旦到了上千条列表页接口响应时间会从几秒优化到几百毫秒。门诊高峰期、号源列表这类高频查询我还会进一步加上缓存见下一节。5.2 高频接口的缓存策略整个系统里查询压力最大的接口一般是科室排班查询、号源余量查询和患者基本信息查询。这三类数据有两个特点变化频率不高、单次查询复杂。适合对查询结果做缓存。Django缓存框架配置起来很简单我用的Redis作为后端# settings.py CACHES { default: { BACKEND: django_redis.cache.RedisCache, LOCATION: redis://127.0.0.1:6379/1, OPTIONS: {CLIENT_CLASS: django_redis.client.DefaultClient} } } # 视图函数中 from django.core.cache import cache def doctor_schedule(request, doctor_id, date): cache_key fdoc_schedule_{doctor_id}_{date} data cache.get(cache_key) if data is None: data build_schedule_data(doctor_id, date) cache.set(cache_key, data, timeout300) return JsonResponse(data, safeFalse)这里有一个值得注意的细节缓存失效策略。医生的排班如果被修改了旧的缓存可能还要存活5分钟患者会看到旧排班。我的解决办法是修改排班时主动删除对应缓存键这个操作比被动等待过期要可靠得多。5.3 常见报错与排查建议这个项目我调试过程中踩过不少坑整理成表格方便你对照排查现象可能原因排查与解决建议登录后马上跳转回登录页LOGIN_URL配置错误或Session没有生效检查settings.LOGIN_URL再检查SESSION_ENGINE是否被误改挂号提交时报“数据库锁等待超时”select_for_update()的锁没释放或事务嵌套导致死锁检查视图函数里有没有提前return导致事务没走完减少长事务中不必要的外部调用查询列表加载缓慢未使用select_related或prefetch_related或者数据库缺少索引用Django Debug Toolbar查看SQL数量对外键字段添加db_indexTrue图片、CSS、JS加载不出生产环境未配置静态文件服务执行collectstatic并在Nginx中正确映射STATIC_ROOT外键删除报“受保护”错误业务表使用了PROTECT保护约束这是有意为之设计上应提供“停用/作废”按钮而不是物理删除数据时区显示异常号源时间差了8小时TIME_ZONE与USE_TZ配置不一致明确业务时区建议TIME_ZONEAsia/Shanghai且USE_TZFalse或统一用UTC并做前端转换6. 从开发到上线环境准备与生产部署6.1 本地开发环境准备做这个项目我建议的本地环境是Python 3.10、Django 4.2 LTS、MySQL 8.0或PostgreSQL 14。Django 4.2是长期支持版本安全性修复有保障不要再无脑用Django 2.x。Python这边建议用虚拟环境工具venv或virtualenvwrapper隔离依赖。依赖安装的常用命令python -m venv venv source venv/bin/activate pip install django4.2.* mysqlclient 或 pymysql pip install django-redis gunicorn pip freeze requirements.txt数据库连接建议放环境变量不要写在settings.py里。Django没有内置.env读取但可以用django-environ或者os.environ配合.env文件手动解析。这个小习惯能避免代码仓库泄露生产数据库密码。6.2 GunicornNginx的生产部署部署我采用最经典的GunicornNginx组合。整体结构是Nginx负责静态文件与反向代理Gunicorn负责跑Django进程。部署在云服务器Linux上的具体步骤大致如下将项目代码同步到服务器安装依赖并执行数据库迁移pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput启动Gunicorngunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 60workers数量不要盲目开大推荐是CPU核心数×2再加1太多反而会增加内存压力和数据库连接竞争。Nginx配置示例server { listen 80; server_name your_domain_or_ip; client_max_body_size 10m; location /static/ { alias /path/to/your/project/staticfiles/; } 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设定容易被忽略开启了HTTPS之后如果Nginx在反向代理层结束了HTTPSDjango需要知道客户端用的是HTTPS否则重定向或绝对URL生成会出问题。解决办法就是加SECURE_PROXY_SSL_HEADER (HTTP_X_FORWARDED_PROTO, https)同时设置X_FRAME_OPTIONS DENY防止点击劫持。6.3 上线前功能与安全检查清单推送生产服务器之前我把自己的检查清单分享出来照着过一遍能避免很多低级的线上事故。功能检查所有角色账号能否正常登录挂号全流程走通建档-挂号-看诊-处方-收费-发药退号和退费逻辑是否正确患者列表检索是否覆盖姓名、病历号、手机号权限越权测试比如医生账号访问收费页面是否被拦截。安全检查DEBUG False是否设置ALLOWED_HOSTS是否配置为实际域名或IP静态文件目录是否正确数据库备份策略是否已有服务器防火墙是否只开放80/443端口更换Django的SECRET_KEY不能用开发时的默认值。7. 系统写完后的几点复盘这套系统从设计到写完我复盘的结论是它的难点不在“技术实现”而在“业务建模与边界控制”。你要真正处理的数据状态远比表面看到的字段复杂一个处方从开立到发药要经历多个状态每个状态都要能追责、能回溯。Django帮我们省了大量基础工作但业务洞察和流程管控仍然要自己打磨清楚。最后再分享一个小技巧系统里一定要预留一个“全局操作时间线”功能把所有关键业务操作包括谁、何时、干了什么、改了哪条数据统一输出成列表。这个功能做起来不难但是上线后和医院方对接问题时的效率利器。常规文档里不会写这些但这是我踩过数次坑之后最想让你提前知道的一件事。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询