资讯动态

基于Django的社区诊所电子病历管理系统实战

发布时间:2026/10/21 12:09:26 来源:尧图企业网站定制
社区诊所的电子病历管理系统乍看是个小项目其实牵扯的东西一点不少。最近我带着一个三人小团队用 Python Django 从零搭出了一套居民电子病历管理系统内部代号 fm9032h6覆盖了挂号、病历书写、处方开药、药品库存、统计报表这一整条门诊链路目前已经在两家社区诊所稳定跑了好几个月。这篇文章把整个项目的需求拆解、数据库设计、核心模块实现和上线后踩过的坑完整整理出来给正在做或者准备做类似系统的朋友一份可以直接参考的实战记录。1. 项目整体设计与思路拆解1.1 社区诊所的真实痛点大医院的信息化靠整套HIS系统厂商多、预算足、流程标准。但社区诊所完全是另一回事人员编制紧张几乎没有专职IT预算只能按万算还得天天面对居民的各种就诊需求。我接触的这家社区诊所之前一直是纸质病历档案柜塞得满满的医生复诊时全靠记忆和翻本子效率低而且容易漏信息。真正做需求调研时诊所负责人列了几个核心诉求病历要能快速检索、居民再次就诊时一眼能看到历史记录、开药要和药房库存对得上、月底的报表能自动生成。这些需求翻译成技术语言就是一套基于 Web 的病历数据录入与检索系统带药品库存联动和统计汇总能力操作要简单到五十多岁的医生不用培训太久就能上手。1.2 技术选型为什么不是 Flask 也不是商用 HIS选型阶段我对比了几条路。Flask 灵活但太裸用户认证、Admin后台、ORM 这些都得自己拼FastAPI 适合API服务对这种服务端渲染为主的内部系统帮助有限商用 HIS 系统在社区诊所场景下要么价格超预算要么功能冗余、定制困难。Django 恰好卡在中间ORM 省去大量 SQL 编写工作Admin 后台经过简单定制就能作为内部管理界面使用自带的认证系统直接支撑起角色权限控制。对这种中小型医疗管理系统来说一个全栈开发 Django 的组合可以覆盖绝大部分需求后续加功能也有清晰的扩展路径。1.3 功能范围与模块划分系统上线第一版我圈定了六个核心模块而不是把社区诊所信息化的一切都塞进来。模块核心功能优先级居民健康档案建档、修改、查询、证件号唯一校验高门诊挂号挂号、排队、接诊状态流转高电子病历病历书写、历史记录、打印高处方管理开药、库存校验、发药登记高药品库存入库、出库、低库存预警中统计报表门诊量、病种分布、药品消耗中功能范围划分有一条原则优先做医护人员每天都要用的功能管理类统计功能往后排。第一版上线时报表模块只做了最基础的门诊量统计病种分析和药品消耗分析在第二版迭代里才补上。这样做的目的是缩短上线周期让用户尽早用起来再根据真实反馈调整。2. 数据库设计与模型实现2.1 业务实体梳理与关系设计数据库设计是整个项目里最花时间也最关键的一步。医疗数据的关联关系比普通管理系统复杂得多一个居民可以有多条就诊记录一条就诊记录可以对应多个诊断和多个处方条目一个药品品类可以多次进出库。关系理不顺后面写业务代码就是灾难。我最终的模型关系如下User用户与 Patient居民是一对一关系但并非所有用户都是患者因此用可选的外键或独立的一对一表Patient 与 MedicalRecord病历是一对多MedicalRecord 与 Diagnosis诊断是一对多MedicalRecord 与 Prescription处方是一对一Prescription 与 PrescriptionItem处方明细是一对多Drug药品与 StockRecord库存流水是一对多库存当前值用聚合方式维护而非实时计算。这个设计的核心想法是病历和处方分开建模。有些诊所初诊只是做检查没开药如果处方字段直接嵌在病历表里要么产生大量空字段要么就得用JSON字段勉强撑着。拆成独立表之后病历表保持干净处方表只在有开药行为时才产生记录。2.2 Django 模型代码示例核心模型代码大致如下from django.db import models from django.contrib.auth.models import User class Patient(models.Model): GENDER_CHOICES ((M, 男), (F, 女)) user models.OneToOneField(User, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name关联账号) name models.CharField(姓名, max_length50) id_card models.CharField(身份证号, max_length18, uniqueTrue, db_indexTrue) gender models.CharField(性别, max_length1, choicesGENDER_CHOICES, defaultM) birthday models.DateField(出生日期) phone models.CharField(联系电话, max_length20) address models.CharField(住址, max_length200, blankTrue) allergy_history models.TextField(过敏史, blankTrue) created_at models.DateTimeField(建档时间, auto_now_addTrue) class Meta: ordering [-created_at] verbose_name 居民健康档案 verbose_name_plural 居民健康档案 def __str__(self): return f{self.name} ({self.id_card}) class MedicalRecord(models.Model): patient models.ForeignKey(Patient, on_deletemodels.CASCADE, related_namerecords, verbose_name居民) doctor models.ForeignKey(User, on_deletemodels.PROTECT, verbose_name接诊医生) chief_complaint models.TextField(主诉) present_illness models.TextField(现病史, blankTrue) past_history models.TextField(既往史, blankTrue) physical_exam models.TextField(体格检查, blankTrue) treatment_advice models.TextField(处理意见, blankTrue) created_at models.DateTimeField(就诊时间, auto_now_addTrue) class Meta: ordering [-created_at] indexes [ models.Index(fields[patient, -created_at]), ]外键关系的选择上有个细节值得说医生和病历之间的外键我用了on_deletemodels.PROTECT而不是常见的CASCADE。病历是医疗凭证绝对不能因为用户删除就连带把病历删了。系统里用户可以被禁用但不会被删除从数据安全角度考虑保护操作比方便操作更重要。2.3 几个容易踩坑的设计决策第一是身份证号唯一校验。同一居民可能在两个平台上都以本人身份建档如果不用身份证号做强校验会出现一人多档的问题健康数据就碎片化了。实现上用uniqueTrue加数据库约束双保险同时在前端写一个建档时的实时查询接口提示操作人员居民是否已存在。第二是药品库存的聚合维护。库存牵涉到入库、出库、退货、盘盈亏多种流水如果每次要算库存都把流水表全扫一遍求和数据量大了之后查询会越来越慢。我的方案是药品表上维护一个current_stock整数字段每次出入库操作时在同一事务内更新流水表和聚合字段查询时直接读字段值性能好而且实现简单。第三是 ID 生成方式。病历号我建议用自增主键但对外展示时拼接日期前缀比如202406120001。直接用数据库自增主键做病历号容易被遍历抓取拼接日期再补零的方式既满足可读性又能避免直接暴露数据量。id_card这种敏感字段在列表中同样要做脱敏展示完整证件号只在详情页展示。3. 核心功能模块的实操实现3.1 用户认证与角色权限控制诊所系统里角色至少分四种管理员、医生、药房人员、挂号人员。Django 自带的Group加Permission机制能覆盖这种简单场景不用引入第三方权限库。我定义了三类权限操作view_patient、change_medicalrecord、dispense_prescription分别用 Django 的permission_required装饰器挂在对应的视图函数上。from django.contrib.auth.decorators import permission_required permission_required(clinic.change_medicalrecord, raise_exceptionTrue) def record_create(request, patient_id): ...实际操作中遇到一个问题Django 默认权限只在模型层面做粗粒度控制没法限制医生只能修改自己写的病历。我给 MedicalRecord 增加了一个update_allowed方法在视图中手动判断操作者是否是记录创建者或管理员不是则直接返回403。这样的业务级权限判断逻辑集中在services.py里避免散落在各个视图函数中。3.2 病历书写与历史病历检索病历书写页是整个系统里操作频率最高的页面体验优化直接决定诊所愿不愿意用。我的做法是病历表单默认展示该居民最近三次就诊记录通过 AJAX 局部刷新医生接诊时先看历史再加新记录不用来回跳页面。// 使用 fetch 加载历史病历摘要 fetch(/api/patients/${patientId}/recent-records/) .then(response response.json()) .then(data { const container document.getElementById(history-records); data.records.forEach(record { const card div classrecord-card div classrecord-date${record.date}/div div classrecord-content${record.chief_complaint}/div /div; container.insertAdjacentHTML(beforeend, card); }); });历史病历的检索除了按姓名和证件号我还加了一个全文检索字段。Django 的icontains在数据量小的时候够用但数据量到几万条时性能会明显下降。后来引入了postgresql的全文检索或者用SQLite FTS5按主诉和诊断内容做关键词搜索响应速度快了一个量级。如果项目初期数据量小用icontains先撑住也行但表结构设计时要预留检索字段的位置。3.3 处方开药与药品库存联动处方这块有一点我印象很深开药时不能只做前端库存校验后端必须再次校验否则两个人同时在系统里给同一个患者开同一种药库存会变成负数。Django 里用select_for_update加行锁是标准解法。from django.db import transaction transaction.atomic def create_prescription(request, record_id): ... for item in items: drug Drug.objects.select_for_update().get(pkitem[drug_id]) if drug.current_stock item[quantity]: raise ValidationError(f药品 {drug.name} 库存不足) drug.current_stock - item[quantity] drug.save() PrescriptionItem.objects.create(...)select_for_update在事务内锁定药品行另一个请求要改同一行时必须等锁释放这就彻底避免了超卖。但要注意本地开发用 SQLite 时select_for_update是空操作必须在 MySQL 或 PostgreSQL 上测试并发逻辑否则会误以为没问题。药品有效期也是个容易忽视的字段。入库时记录批次有效期开药时系统只在界面上展示近效期标识不强制拦截。社区诊所本身药品周转快但近效期提醒能防止某些慢病药放到过期才发现。3.4 预约挂号与门诊流程挂号模块的状态流转看着简单实际状态划分要提前想清楚。我用了四态模型待就诊、就诊中、已完成、已取消。挂号人员创建挂号记录状态为待就诊医生点击接诊后变就诊中此时病历表单解锁保存病历并开完处方后变已完成患者未到或取消时走已取消状态且已取消的号不可再回退。这个状态机在视图层用一个独立的transitions.py模块维护每个状态变更都做合法性校验避免出现已完成状态还能继续开药这种逻辑漏洞。排号队列用简单的queue_position整数字段实现每天零点按日期重置计数不需要引入消息队列复杂度控制在了合理范围。3.5 统计报表与健康档案导出报表模块第一版我只做了三个日门诊量、月度病种分布、药品消耗排行。实现思路是聚合查询加缓存日报表数据当天有效直接用 Django 的cache_page对接口做缓存过期时间设为半小时。病种分布统计用annotate加Count按诊断名称分组数据量小的时候几十毫秒出结果完全没必要上数据仓库那套东西。健康档案导出用 CSV 格式考虑到后续要对接公共卫生平台还预留了标准化的导出字段映射。有一个经验导出功能一定要加时间范围和字段范围的选择不然每次全量导出既慢又容易被平台驳回。4. 安全与合规重点处理4.1 医疗数据的安全底线做医疗系统数据安全是不能讨价还价的。虽然社区诊所不是等保三级单位但涉及到居民个人信息和病历数据最基本的要求必须达到传输层面强制 HTTPS密码存储使用 Django 默认的 PBKDF2 加密不自行设计加密方案身份证号、手机号在列表页脱敏显示操作日志记录关键动作包括谁在什么时间查看了谁的病历。操作日志我用了一个轻量方案Djangosignal监听MedicalRecord的post_save同时设计一个PatientAccessLog表在查看病历详情页时写入访问记录。有了日志一旦出现隐私泄露争议好歹能追溯。4.2 Django 安全配置实践Django 项目的安全配置有一些现成的坑。DEBUGFalse是上线前提不然报错页面会直接暴露服务器路径和配置信息。ALLOWED_HOSTS必须明确设置CSRF_COOKIE_SECURE和SESSION_COOKIE_SECURE在 HTTPS 环境下要置为 True。文件上传是容易被忽略的环节。病历中的检查报告图片我限制只能上传常见图片格式并校验文件实际内容头而不是只看扩展名。同时上传目录不允许执行任何脚本文件静态文件与上传目录分离。这些都是标准做法但实际操作中确实见过有人把用户上传目录直接放在项目静态目录下风险极大。5. 常见问题与排查技巧实录5.1 数据库锁与事务超时上线后遇到第一个比较头疼的问题是处方开药偶发报错DatabaseError: Deadlock found。排查过程发现问题出在两个事务交叉申请药品锁上——医生A先锁药品X再锁药品Y医生B先锁药品Y再锁药品X两个事务互相等待就死了。解决方式所有涉及多药品库存扣减的事务先把药品按 ID 排序再依次加锁保证锁顺序全局一致。另外给 MySQL 的innodb_lock_wait_timeout设一个合理值默认50秒太长我调整为5秒出问题快速失败而不是让请求长时间挂着。5.2 慢查询定位与索引优化上线两周后门诊量增长病历列表页开始变慢。打开 Djangodjango-db-geventpool的日志后定位到一条全表扫描的查询。原因很简单MedicalRecord表按created_at做范围过滤时联合索引没建对。# 修正后的索引 models.Index(fields[patient, -created_at]),原本的索引顺序是created_at在前面过滤patient时索引就没用上。调整顺序后该查询从几百毫秒降到了十几毫秒。排查思路可以分享先开django.db.backends日志看执行的SQL再配合EXPLAIN看是否走了索引大部分性能问题都能通过这两个步骤定位。5.3 文件上传的内存与路径问题检查报告图片上传时默认 Django 会把小于FILE_UPLOAD_MAX_MEMORY_SIZE默认2.5MB的文件加载到内存大于则写入临时文件。当时没有注意到这点连续上传大图时内存飙升。解决方式是将FILE_UPLOAD_MAX_MEMORY_SIZE调整为1MB配合前端压缩把图片控制在合理体积。路径还有个坑Windows 开发环境和 Linux 生产环境用os.path.join拼接路径没问题但千万别用字符串加号拼路径反斜杠和斜杠的差异会让生产环境直接404。5.4 时区与日期分组统计的偏差报表统计按天汇总时出现过日期偏移一天的诡异问题。原因是 Django 的USE_TZTrue且TIME_ZONEAsia/Shanghai配置不一致导致created_at以 UTC 存储而分组统计时又按本地日期截断。from django.db.models.functions import TruncDate from django.utils import timezone daily_count (MedicalRecord.objects .filter(created_at__datetimezone.localdate()) .count())最终统一用django.utils.timezone.localdate()做日期边界判断并且规定数据库连接串里强制OPTIONS时区为系统时区。这个坑排查了很久主要原因是本地开发时 SQLite 对时区处理不敏感MySQL 一上就立刻暴雷。6. 部署上线与环境运维6.1 部署架构与实践流程部署架构我用了比较常规的组合Nginx Gunicorn MySQL部署在一台2核4G的云服务器上。社区诊所日门诊量峰值在200人左右这个配置绰绰有余。静态文件由 Nginx 直接服务Django 只处理动态请求Gunicorn 配置4个 worker每个 worker 处理完请求后自动回收。# gunicorn 启动命令示例 gunicorn clinic_project.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 4 \ --timeout 60 \ --access-logfile /var/log/gunicorn/access.log \ --error-logfile /var/log/gunicorn/error.log部署流程我用 Git 加简单的 shell 脚本实现。代码推送到仓库后服务器上执行拉取、迁移、收集静态文件、重载 Gunicorn 四步操作。没有上容器化这种规模的项目上 Docker 反而是过度设计。6.2 数据备份与恢复策略医疗数据丢失是不可逆事故备份策略必须提前定好。我用的是mysqldump crontab每天凌晨3点做全量备份保留最近7天的备份文件另外每周把备份文件同步一份到异地对象存储。# 每日备份脚本 /usr/local/bin/backup_db.sh #!/bin/bash BACKUP_DIR/data/backup/mysql DATE$(date %Y%m%d_%H%M%S) mysqldump -u backup_user -p$DB_PASSWORD \ --single-transaction --quick \ clinic_db | gzip $BACKUP_DIR/clinic_db_$DATE.sql.gz find $BACKUP_DIR -name *.sql.gz -mtime 7 -delete此外还有一个易被忽略的点Media上传目录里的检查报告图片不能只靠数据库备份因为数据库只存了图片路径图片文件本身在文件系统里。我备份脚本里把media目录做了增量同步用rsync到备份机这个动作虽然不起眼但关键时刻能救命。7. 上线后的几点真实体会系统在诊所里跑起来之后最大的感触是技术只解决了一半问题。医生刚开始对电子病历是抵触的觉得打字比手写慢。后来我把病历模板功能加上去常见病种用模板预填医生只需要改少量内容使用率立刻上来了。所以做这类管理系统一定要深入观察真实工作流功能设计要嵌进流程里而不是让流程迁就系统。还有一次印象深刻的教训某天诊所反馈挂号页打不开排查了半小时才发现是服务器磁盘满了原因是访问日志没有做轮转切割。从那以后日志轮转成为部署脚本的一部分这类基础设施问题虽然和业务代码无关但出一次事就能浪费半天时间。如果后续要继续扩展我的建议是优先做两件事一是居民端微信小程序预约和报告查询把居民从线下排队中解放出来二是对接上级公共卫生平台的数据上传接口让社区诊所在公共卫生体系中把数据真正用起来。功能扩展前记得先把接口层抽象好别让业务代码和外部系统耦合得太死。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑