ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+Django街区医院管理系统:挂号、收费、发药全解析

Python+Django街区医院管理系统:挂号、收费、发药全解析 简介一份面向Python毕业设计的街区医院管理系统完整项目内含毕业论文与可运行源码。系统基于Python语言、Django框架和MySQL数据库采用B/S架构实现管理员、医生、患者三类角色的注册登录、信息管理与业务处理功能。论文部分从选题背景、需求分析到数据库设计、模块实现、系统测试均有详细阐述并附有系统流程图、E/R图、数据库表及测试用例适合计算机相关专业学生参考。压缩包大小约34.66MB为zip格式内容涵盖项目源码与论文文档。源码中Django项目结构、模板文件、静态资源及数据库脚本可供直接运行或二次开发论文文档亦可编辑修改。目前已有54人学习。下载后可获得完整课题方案、可运行代码与论文框架能帮助理清街区医院管理系统的开发流程节省选题与搭建时间也可作为答辩准备和功能扩展的基础。1. 从手工记账到 Python 街区医院管理系统别把毕设做成 CRUD街区医院的日常听起来不复杂挂号、就诊、开方、收费、发药。但靠 Excel 和纸质处方运转时各个环节是断开的挂号窗口不知道今天哪位医生还有号源药房不知道库存何时见底月底对账只能逐条人工核对。这个基于 Python 的街区医院管理系统把患者档案、医生排班、挂号记录、处方明细、收费流水、药品库存全部串进一套 Web 系统用 Django 做后端MySQL 存数据页面由模板直接渲染。对做 Python 毕业设计的同学来说这是最能体现水平的选题类型它以完整业务链路覆盖 ORM、状态机、事务、并发等论文里会被追问的技术点源码包自带论文读时可以逐章对照代码理解需求分析和数据库设计。适合正在找 python 课程设计、毕业设计参考的本科生也适合给社区诊所搭建管理原型的开发者。2. Django 模型设计与 MySQL 表结构先把数据地基打牢2.1 选 Django 而不是 Flask 的理由接到这个题目时很多人第一反应是找个 Flask 模板改一改。Flask 确实轻但街区医院管理系统最终要交付的不是一个接口 Demo而是一套带页面、带权限、带后台管理的完整系统并且论文里要有系统架构图、功能模块图、数据库表结构说明。Django 的 MTV 架构可以对应论文里的“表现层-业务层-数据层”自带的 Admin 后台可以免开发直接截图ORM 迁移机制让数据库结构由模型代码生成论文的数据库设计章节不会出现“代码和表结构对不上”的情况。选型时还可以和 FastAPI 对比一下。FastAPI 擅长异步接口和 API 文档但它的默认形态是返回 JSON不太适合“医生点按钮、收费员敲键盘”的多页面操作场景。如果非要用 FastAPI前端还得搭配 Vue 或 React技术栈会膨胀答辩时被问到“前后端分离的边界在哪里”很容易露怯。这个项目我一般用 python 3.10 搭配 Django 3.2 LTSJinja2 模板渲染页面依赖安装简单pip install -r requirements.txt一条命令就能复制出环境。提示python 版本和 Django 主版本尽量选稳定组合不要为了追新用刚发布的大版本模板语法和第三方库兼容性在毕设答辩期容易出问题。2.2 核心实体关系从业务主线拆表街区医院的业务主线是患者建档 → 挂号 → 医生就诊 → 开处方 → 收费 → 发药 → 扣库存。沿着这条线需要先把数据表拆出来表名用途关键字段patient患者档案name, gender, id_card, phone, created_atdepartment科室name, location, descriptiondoctor医生name, title, department_id, max_registration, scheduleregistration挂号记录patient_id, doctor_id, register_date, period, statusprescription处方registration_id, diagnosis, is_paid, created_atprescription_item处方明细prescription_id, drug_id, quantity, amountdrug药品name, spec, price, stock, unitcharge_record收费记录registration_id, amount, pay_method, created_at这几张表的关系是患者和挂号一对多医生和挂号一对多挂号与处方一对一处方与药品多对多并通过处方明细表关联收费记录与挂号一对一。为什么处方明细要单独成表因为一张处方可能开三种药每种药的单价、数量都不一样。如果只在 prescription 里放一个总金额收费时就没法追溯“这笔钱是哪几种药组成的”药房发药时也不知道该拿哪几种药。所以中间的 prescription_item 表是必要的它的 amount 字段可以在开方时带出也可以在收费时统一计算。2.3 模型代码怎么落先定义基础资料模型科室和医生属于最稳定的静态数据from django.db import models class Department(models.Model): name models.CharField(科室名称, max_length50) location models.CharField(位置, max_length100) def __str__(self): return self.name class Doctor(models.Model): TITLE_CHOICES [ (junior, 住院医师), (attending, 主治医师), (chief, 主任医师), ] name models.CharField(姓名, max_length30) department models.ForeignKey( Department, on_deletemodels.PROTECT, verbose_name所属科室, ) title models.CharField(职称, max_length20, choicesTITLE_CHOICES, defaultjunior) max_registration models.PositiveIntegerField(单时段最大号源数, default20) schedule models.CharField(出诊时间, max_length100, blankTrue)on_deletemodels.PROTECT表示科室下面还有医生时不允许删除避免档案级联丢失这一条在论文“数据完整性设计”里可以单独写一段。max_registration用PositiveIntegerField相当于在数据库层加非负约束比在视图里手动判断更安全。choices枚举值在 Admin 后台会渲染成下拉框演示时不用费劲解释取值范围。然后是挂号记录这是整个系统里状态最复杂的模型class Registration(models.Model): STATUS_CHOICES [ (pending, 待就诊), (in_progress, 就诊中), (finished, 已完成), (cancelled, 已取消), ] patient models.ForeignKey(Patient, on_deletemodels.CASCADE, verbose_name患者) doctor models.ForeignKey(Doctor, on_deletemodels.PROTECT, verbose_name医生) register_date models.DateField(就诊日期) period models.CharField(时段, max_length2, choices[(am, 上午), (pm, 下午)]) status models.CharField(状态, max_length10, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return f{self.patient.name}-{self.doctor.name}-{self.register_date}auto_now_addTrue会在创建时自动写入当前时间人工不需要传这个值。status字段是系统的状态枢纽后续的收费模块只允许对“已完成”的挂号生成收费记录药房发药也只允许对已缴费的处方操作这样整个流程不会出现“还没看病就收费”的情况。最后是处方主表。校区医院里一张处方对应一次就诊所以用OneToOneField而不是普通外键class Prescription(models.Model): registration models.OneToOneField(Registration, on_deletemodels.CASCADE, verbose_name挂号) diagnosis models.TextField(诊断结果) is_paid models.BooleanField(是否已缴费, defaultFalse) dispensed_at models.DateTimeField(发药时间, nullTrue, blankTrue)模型定义完需要同步到 MySQLpython manage.py makemigrations hospital python manage.py migrate第一次运行makemigrations会生成一个迁移文件它只记录表结构的变更不真正写数据库migrate才是把变更应用到 MySQL。如果改了模型却忘记生成迁移直接runserver会报模型和实际表结构不一致的错误。新建开发库后只要跑完这两条命令表结构和模型就同步了。3. 挂号、收费、发药三模块实现把业务链路串起来3.1 挂号模块检查号源、生成凭条、维护状态挂号是系统的第一个入口。前端表单拿到patient_id、doctor_id、period、register_date之后后端要做的不只是创建一条记录还要先检查这个医生、这个时段是否还有号源。我先给出一个不带锁的直观写法def create_registration(patient_id, doctor_id, period, register_date): doctor Doctor.objects.get(pkdoctor_id) current_count Registration.objects.filter( doctordoctor, periodperiod, register_dateregister_date, ).exclude(statuscancelled).count() if current_count doctor.max_registration: raise ValueError(该时段号源已满请选择其他时段) return Registration.objects.create( patient_idpatient_id, doctordoctor, periodperiod, register_dateregister_date, )这个函数放在services.py里视图只负责调用。这样做的直接好处是可以脱离 HTTP 请求写单元测试论文里的功能测试章节可以直接引用这个函数。exclude(statuscancelled)是关键已取消的挂号不占号源如果不排除系统跑一天后所有时段都会显示满号。register_date由前端传值医生没有排班的日期在页面下拉框里就不会出现。视图和路由对应如下# views.py from django.shortcuts import render, redirect from django.contrib import messages def registration_create(request): if request.method POST: try: reg create_registration( patient_idrequest.POST[patient_id], doctor_idrequest.POST[doctor_id], periodrequest.POST[period], register_daterequest.POST[register_date], ) messages.success(request, f挂号成功凭条号{reg.id:06d}) return redirect(registration_detail, pkreg.id) except ValueError as e: messages.error(request, str(e)) return render(request, registration_form.html, {doctors: Doctor.objects.all()}) # urls.py urlpatterns [ path(registration/new/, views.registration_create, nameregistration_create), path(registration/int:pk/, views.registration_detail, nameregistration_detail), ]凭条号直接取主键并格式化成 6 位比如000012。街道小诊所不需要全局唯一的订单号用主键最省事如果业务要求更高可以用 uuid但论文里的表结构会因此多出一个字段。挂号状态只能沿固定方向流转操作原状态新状态操作模块提交挂号-pending挂号医生叫号pendingin_progress就诊医生完成诊断并开方in_progressfinished就诊患者取消挂号pendingcancelled挂号所有修改状态的位置都需要校验原状态pending不能直接跳到finished这条规则对应的就是论文里的业务流程图。3.2 收费模块以处方明细为准不要手填总价收费模块最容易出现的实现错误是让收费员手填金额。正确做法是遍历处方明细逐项计算然后一次性生成收费记录。先补充处方明细模型class PrescriptionItem(models.Model): prescription models.ForeignKey(Prescription, on_deletemodels.CASCADE, verbose_name处方) drug models.ForeignKey(Drug, on_deletemodels.PROTECT, verbose_name药品) quantity models.PositiveIntegerField(数量, default1) amount models.DecimalField(金额, max_digits10, decimal_places2, default0)计算金额的函数def calculate_prescription_amount(prescription_id): items PrescriptionItem.objects.filter( prescription_idprescription_id ).select_related(drug) total 0 for item in items: item.amount item.drug.price * item.quantity item.save() total item.amount return totalselect_related(drug)会把关联的药品数据一次性查出来避免循环里每条明细都发一次 SQL这就是最常见的 N1 查询问题。金额字段用DecimalField而不是FloatField因为浮点数计算 0.10.2 会出现尾数误差金额做累加时差一分钱很难排查。生成收费记录时要把“创建流水”和“标记已缴费”放进同一个事务from django.db import transaction transaction.atomic def do_charge(registration_id, pay_method): reg Registration.objects.select_for_update().get(pkregistration_id) if reg.status ! finished: raise ValueError(患者尚未完成就诊不能收费) prescription Prescription.objects.select_for_update().get(registrationreg) if prescription.is_paid: raise ValueError(该处方已经收费请勿重复操作) total calculate_prescription_amount(prescription.id) ChargeRecord.objects.create( registrationreg, amounttotal, pay_methodpay_method, ) prescription.is_paid True prescription.save() return totaltransaction.atomic保证创建收费记录和更新处方缴费状态要么都成功要么都失败。两个select_for_update分别在 InnoDB 层锁住挂号行和处方行防止两个收费窗口同时操作同一个处方。3.3 药房发药库存不足不能发未缴费更不能发药房发药的校验逻辑是有顺序的先确认处方已经缴费再检查每种药品的库存最后才扣减。只要有一个条件不满足整个发药动作就要停下来。def dispense_drug(prescription_id): prescription Prescription.objects.select_related(registration).get(pkprescription_id) if not prescription.is_paid: raise ValueError(处方未缴费不能发药) items PrescriptionItem.objects.filter(prescriptionprescription).select_related(drug) for item in items: drug item.drug if drug.stock item.quantity: raise ValueError(f{drug.name} 库存不足) drug.stock - item.quantity drug.save() prescription.dispensed_at timezone.now() prescription.save()这里用的是“读出来→判断→写回去”的方式逻辑好理解但在并发场景下会有丢减问题。两个窗口同时发同一种药各自读到 stock5各自扣 1最后写回的都是 4实际却发出去两盒。这个问题的标准解法我放在下一章配合号源并发一起处理。4. 权限、并发与事务答辩时最容易追问的三个技术点4.1 角色权限用 Django Group 而不是拍脑袋加字段街区医院里至少有三种角色挂号员、医生、收费员再加上管理员。常见错误是在 User 表加一个role字段然后代码里到处if user.role doctor。这样能跑但论文里“权限设计”一节会显得单薄。Django 自带auth.Group用分组加装饰器控制页面访问是更规范的常见做法。from django.contrib.auth.decorators import login_required, user_passes_test def is_doctor(user): return user.groups.filter(namedoctor).exists() def is_cashier(user): return user.groups.filter(namecashier).exists() login_required user_passes_test(is_doctor) def doctor_prescription(request, registration_id): ...login_required先拦截未登录用户user_passes_test(is_doctor)再校验角色。两个装饰器有顺序要求反过来的话未登录用户会先被打回登录页逻辑上虽然也能跳转但会暴露一次判断的差异。groups.filter(namedoctor)里的 group 名称需要在 Admin 后台创建并把用户加入对应分组代码里不负责自动建组。4.2 号源并发count 判断会怎么被穿破第三章的create_registration有一个并发漏洞两个请求同时查到current_count 9而max_registration 10双方都认为还剩一个号结果都去创建记录实际挂号变成 11 条。要解决这个问题最稳妥的办法是引入号源表把剩余号源变成数据库里的一行数据class Schedule(models.Model): doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE, verbose_name医生) work_date models.DateField(出诊日期) period models.CharField(时段, max_length2, choices[(am, 上午), (pm, 下午)]) remaining models.PositiveIntegerField(剩余号源, default20)扣减号源时用条件更新不用“先查再改”from django.db.models import F rows Schedule.objects.filter( doctordoctor, work_dateregister_date, periodperiod, remaining__gt0, ).update(remainingF(remaining) - 1) if rows 0: raise ValueError(号源已满)update会在数据库层面把remaining减 1F(remaining)保证读取和写入是原子操作不受 Python 端旧值缓存影响。两个请求同时进来时InnoDB 会锁住这一行第二条等待第一条提交后再判断remaining__gt0不会出现都减成功的场景。这种模式同样适用于药品库存扣减比drug.stock - 1少了一个时间窗口。提示filter(...).update(...)返回的是受影响行数不是对象列表。返回 0 就说明条件不满足这条逻辑是后续所有“防超卖”判断的核心依据。4.3 事务收费和发药必须同时成或同时败收费成功但扣库存失败账目和实物就对不上库存扣了但收费失败就会出现白拿药。常见做法是把收费和发药放进同一个事务任一步抛错就整体回滚。transaction.atomic def charge_and_dispense(registration_id, pay_method): do_charge(registration_id, pay_method) prescription Prescription.objects.get(registration_idregistration_id) dispense_drug(prescription.id)do_charge和dispense_drug内部都用了select_for_update但整个流程处在外层同一个事务里两个行锁按顺序获取只要所有事务都保持“先收费、后发药”的访问顺序就不会形成死锁。如果出现循环等待多半是某个函数改了顺序。这个消息可以在 shell 里直接验证回滚是否生效python manage.py shell -c from hospital.services import charge_and_dispense try: charge_and_dispense(1, cash) except ValueError as e: print(回滚成功:, e) 可以故意把某个药品的库存改成 0再执行上述命令。如果ChargeRecord表中没有新增记录说明transaction.atomic让收费动作跟着回滚了这个操作答辩时现场做一次比截图有说服力得多。5. 源码运行与自动化验证让毕设当场跑通5.1 本地启动步骤拿到源码之后先在项目根目录创建虚拟环境把依赖装进隔离环境里避免污染全局 pythonpython -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser python manage.py runservermigrate会按照第二章的模型自动建表createsuperuser之后才能进入 Admin 后台配置科室、医生、药品。启动后先打开http://127.0.0.1:8000/admin录入基础数据再访问挂号页面做业务操作顺序反了会出现医生列表为空。5.2 造演示数据用 bulk_create 而不是循环 save演示时如果只有三五条患者数据答辩画面会很单薄。用bulk_create一次插入几百条患者档案from hospital.models import Patient Patient.objects.bulk_create([ Patient( namef测试患者{i}, gendermale, id_cardf11010119900101{i:04d}, phonef1380000{i:04d}, ) for i in range(1, 501) ])bulk_create只在最后批量写一次数据库比循环里逐个save快一个数量级500 条数据在本地基本无感。批量插入时要保证表没有外键依赖Patient 是独立表可以直接这样造。5.3 用 TestCase 跑通全链路依赖 service 函数的设计可以直接写一段端到端测试覆盖“挂号—完成就诊—收费—发药”的完整链路from django.test import TestCase from .services import create_registration, do_charge, dispense_drug class FullFlowTest(TestCase): def test_registration_charge_dispense(self): reg create_registration( patient_idself.patient.id, doctor_idself.doctor.id, periodam, register_date2025-05-20, ) self.assertEqual(reg.status, pending) reg.status finished reg.save() total do_charge(reg.id, cash) self.assertGreater(total, 0) prescription reg.prescription dispense_drug(prescription.id) self.assertIsNotNone(prescription.dispensed_at)测试没有走 HTTP 请求而是直接调用 service 函数跑得快、定位准。在项目根目录执行python manage.py test看到 OK 之后回到 Admin 后台刷新 ChargeRecord 列表刚才那条收费记录会出现在第一行演示时这个动作很直观。本文还有配套的精品资源点击获取
返回列表