最近帮某医院信息科做系统维护时我看到了一个很典型的场景药房阿姨面前摊着三本厚厚的台账一本记入库一本记出库还有一本专门抄近效期药品每个月末还要花两天时间手工盘库存。数据不准带来的连锁反应很明显——门诊医生说某种药有库存药房却发不出来一查发现是效期过了还没下账另一边采购凭经验补货热门药积压到过期冷门药反而断货。这种痛感太真实了。所以我把“Django基于Python的新型医院药物管理系统的设计与实现”整个过程中踩过的坑、做过的决策、写过的核心代码都整理成这篇文章。如果你也要做医院药房、社区诊所或门诊部的药物管理系统无论是毕设课题还是实际落地项目这篇内容都能帮你少走不少弯路。我会尽量把“为什么这样设计”讲透而不是只丢给你一堆CRUD代码。1. “新型”到底新在哪传统药房管理的三个典型困境1.1 从Excel和手写处方说起很多中小型医疗机构对药品的管理远没有大家想象中那么“数字化”。我见过用Excel记出入库的也见过完全靠手写处方人工划价的。这种模式的问题在平时可能不明显一旦药品品种超过三四百种或者门诊量上来之后整个链条就开始失控。最典型的是库存账实不符。入库单来了随手记一笔发药的时候忙起来忘了减账月底一盘点电脑里的库存和柜子里的实物差了上百盒。追查的时候根本无从下手因为没有任何操作留痕到底是入库漏记、发药漏减还是有人借药没登记完全靠猜。处方追溯也是老大难。患者拿手写处方来取药药剂师发完药之后这张处方往往就夹在厚厚的纸堆里。万一出现用药纠纷想查“这盒药是哪个医生开的、哪个药剂师发的、哪个批次的”几乎没有可能。1.2 医院药物管理系统要解决的核心问题我把需求拆到最后本质上就三个问题库存可信、效期可控、处方可溯。药物管理系统要做的就是围绕这三件事把原本断裂的流程串成一个闭环。先说库存可信。所有药品的入库、出库、退药、盘点都必须实时更新库存数量任何一步操作都要留痕。库存数据不是给人看的是给系统决策用的所以准确性是底线。再说效期可控。药品和普通商品最大的区别就是有有效期。系统必须做到近效期提前预警、过期药品自动标记、发药时主动拦截过期批次。这一块做不好前面库存管理做得再漂亮也会出医疗事故。最后是处方可溯。从医生开方到药师审核再到发药出库整条链路要有清晰的状态流转和操作记录。出了问题可以倒查是谁在什么时间做了什么操作一目了然。我的理解里“新型”两个字不是噱头而是指这套系统不再像传统台账那样只做“记录”而是以状态流转、权限隔离、实时数据联动为核心逻辑去驱动业务流程。1.3 系统目标与适用场景这套系统适合什么场景我的定位是中小医院的门诊药房、社区医疗中心、校医院以及各类专科诊所。这类机构的特点是药品品种几百到上千种科室不多业务流程相对标准但又养不起HIS厂商那套动辄几十万的大系统。这里要划一条边界药物管理系统不等于合理用药审核系统。它管的是“药在哪、有多少、什么时候过期、谁发给了谁”不是“这个药和那个药能不能联用、剂量是否超限”。后者属于临床药学系统或HIS的高级模块复杂度完全不在一个量级上。我当时做系统边界时特意把这些内容排除在外不然项目根本收不了尾。2. 为什么是Django选型时的横向对比与取舍2.1 自带Admin后台直接省掉一半管理页面医院药物管理系统这种项目天然就分两类页面一类是给医生、药剂师用的业务操作页面需要严格定制另一类是药品主档、供应商、用户等基础数据的维护页面这些页面就是纯粹的增删改查。Django自带的后台Admin对于第二类页面几乎是现成的。把模型写好后在admin.py里注册一下就能获得列表筛选、搜索、分页、表单校验的能力。药品主档维护、供应商信息管理、用户角色分配这些功能完全不需要额外开发前端页面。我见过不少人做这类系统时一上来就用前后端分离架构Vue加Restful API每个管理页面都写一遍表格组件。不是说不行而是对于中小项目的交付速度来说性价比太低了。药房管理员的需求很简单——能查、能改、能导出Admin后台的体验其实已经完全够用。省下来的时间可以投入到业务核心逻辑上。2.2 ORM与Auth体系避免重复造轮子Django的ORM在同类框架里属于“重但好用”的类型。对于药物管理系统这种数据关联极其密集的场景ORM的外键关联、预取查询、聚合统计能力非常顺手。最典型的是查询一张处方时要同时关联患者、医嘱、药品批次、发药人等多个维度用ORM的select_related和prefetch_related写起来非常自然既不用拼SQL字符串也不容易漏字段。另一个关键是Django内置的认证授权体系。用户登录、会话管理、密码哈希、权限校验、用户组框架全都内置。医院系统对权限隔离的要求很高医生和药剂师的操作范围必须严格区分Django的Group加Permission机制正好能覆盖这个需求。2.3 与其他方案的对比我在做技术选型时其实也权衡过其他路线。这里给出当时的横向对比供参考方案优势劣势适用场景Django PostgreSQL全家桶完整ORM成熟自带Admin和Auth学习曲线略陡灵活性不如Flask中小型管理系统最省心Flask SQLAlchemy轻量灵活自由组合认证、后台、ORM都要自己拼对定制要求极高的小团队Spring Boot企业级生态性能好开发效率低Java上手成本高大型HIS集成团队以Java为主前后端分离 Node.js前端体验好要同时维护两套工程成本翻倍交互复杂度远超后台管理的场景对于一个以数据管理和流程流转为核心、页面交互不算复杂的系统Django的“全家桶”模式几乎是效率最优解。我最终选了Django搭配PostgreSQLRedis用来做缓存和Celery的消息队列。2.4 项目结构规划项目结构上我没有选择把全部业务塞进一个app里而是按领域拆成了几个appdrug药品主档和库存、purchase采购入库、prescription处方与发药、supplier供应商、report统计报表。这样划分的好处是代码边界清晰后续加功能时不会牵一发动全身。每个app内部维持model、view、url、service四层业务逻辑尽量下沉到service层视图只负责参数校验和响应组装。3. 数据模型的拆与合药品、库存、批次的边界划分3.1 药品主档与库存批次为什么要拆成两张表这是我这次设计里最想详细说的一点。很多人做药品管理时图省事会把“药品”设计成一张大表字段包含名称、规格、厂家、库存数量、有效期、进价……看起来所有信息都在一张表里了查询也方便但用一段时间就会出问题。问题出在批次上。同一盒阿莫西林胶囊同一个规格半个月前进货的批次和上个月进货的批次有效期可能差两三个月进价也可能不一样。如果你把这些信息全塞在药品主档里那么新批次入库时旧批次的信息就被覆盖了。你根本不知道当前库存里哪部分是哪个批次的近效期预警也就无从谈起。所以我把数据模型拆成了两块药品主档Drug管“是什么药”库存批次DrugStock管“具体哪一批药”。药品主档记录通用名、商品名、规格、剂型、生产厂家、批准文号等静态属性库存批次记录批号、生产日期、有效期、进价、当前数量、所在库房。两者是一对多的关系。我当时和一个同学讨论时打了个比方药品主档好比是“阿莫西林胶囊”这个商品条目库存批次则是货架上的每一批具体货物。超市盘点时不会只数商品种类一定是一批一批地数。药房也是这个道理。3.2 核心模型字段设计模型是这套系统的地基我贴出核心的几段代码字段和注释都保留了当时的设计思路from django.db import models from django.contrib.auth.models import User class Drug(models.Model): 药品主档只维护药品的静态信息不关心库存和批次。 drug_code models.CharField(max_length32, uniqueTrue, verbose_name内部药品编码) generic_name models.CharField(max_length128, verbose_name通用名) trade_name models.CharField(max_length128, blankTrue, verbose_name商品名) specification models.CharField(max_length128, verbose_name规格) dosage_form models.CharField(max_length32, verbose_name剂型) manufacturer models.CharField(max_length128, verbose_name生产厂家) approval_number models.CharField(max_length64, blankTrue, verbose_name批准文号) unit models.CharField(max_length16, verbose_name基本单位) reference_price models.DecimalField(max_digits10, decimal_places2, default0, verbose_name参考单价) is_active models.BooleanField(defaultTrue, verbose_name是否在售) def __str__(self): return f{self.generic_name} {self.specification} class Meta: db_table drug verbose_name 药品主档 class DrugStock(models.Model): 库存批次每一批药品的批次信息与实时数量。 drug models.ForeignKey(Drug, on_deletemodels.PROTECT, related_namestocks, verbose_name药品) batch_no models.CharField(max_length64, verbose_name生产批号) production_date models.DateField(verbose_name生产日期) expiry_date models.DateField(verbose_name有效期至) quantity models.DecimalField(max_digits12, decimal_places3, default0, verbose_name当前库存数量) purchase_price models.DecimalField(max_digits10, decimal_places2, default0, verbose_name批次进价) location models.CharField(max_length64, blankTrue, verbose_name存放位置) is_locked models.BooleanField(defaultFalse, verbose_name是否锁定) def __str__(self): return f{self.drug.generic_name}-{self.batch_no} class Meta: db_table drug_stock verbose_name 药品库存批次 unique_together (drug, batch_no, expiry_date) class Supplier(models.Model): name models.CharField(max_length128, verbose_name供应商名称) contact models.CharField(max_length32, blankTrue, verbose_name联系人) phone models.CharField(max_length32, blankTrue, verbose_name联系电话) address models.CharField(max_length255, blankTrue, verbose_name地址) remark models.TextField(blankTrue, verbose_name备注) class Meta: db_table supplier verbose_name 供应商这里有几个字段设计上的细节都是实际踩过坑之后才确定的第一个是数量字段用DecimalField而不是FloatField。药品数量会涉及小数比如某些口服液按瓶计、某些针剂按支计还有一种情况是拆零药品按片计0.5片这种数量会出现。Float在累加时会有精度误差而库存数据是绝对不能有误差的。第二个是unique_together设了drug, batch_no, expiry_date。同一药品、同一批号、同一有效期只能有一条库存记录防止重复录入批次导致库存虚增。药品主档里也用drug_code做了唯一约束而不是用名称做约束因为不同厂家生产的同一种药商品名可能完全不同名称作为唯一键一定会撞。第三个是外键用on_deletemodels.PROTECT。药品主档一旦被库存批次引用就不允许被物理删除。如果有药品确实停用用is_active字段来标记就行。保护模式能在数据完整性上提供一层底线。3.3 单据与库存的历史快照很多人忽略的一环采购单据的设计我踩了一个不大不小的坑值得单独说一下。采购入库单PurchaseOrder和入库明细PurchaseItem肯定要记录“进了什么药、进了多少、从哪个供应商进的”。但我最初的设计里入库明细只关联了药品外键没有冗余药品名称、规格、进价等快照字段。后来做报表统计时发现问题了前端展示的入库明细需要实时关联药品表才能把名称和规格显示出来一旦某个药品停用后被改了规格或厂家历史单据上显示的就会变成新信息而当时的单据记录反而丢失了。这不是一个小问题因为药房所有的追溯都要以“当时发生的数据”为准。正确的做法是冗余快照字段。在PurchaseItem里除了drug外键同时保存drug_name、specification、purchase_price、batch_no等字段。这样历史单据永远忠实于当时发生的事实不随主档变化而漂移。处方明细PrescriptionItem同样如此每条明细都要保存药品名称、规格、单价、用量。这是审计类系统的基本要求我建议所有做单据业务的都记住这个原则匹配到主档负责当下的关联冗余字段负责历史的真实。4. 处方流转与发药闭环把“医生开药”变成一条可追溯的链路4.1 处方状态机设计药物管理系统里最复杂的业务流程就是处方流转。从医生开方到患者取药中间涉及审核、发药等多个环节如果任由开发人员随心所欲地改数据库字段整个流程很快就失控了。我采用的方式是给处方定义一套明确的状态机状态含义允许的下一步操作pending已开方待药房审核审核通过或作废approved审核通过待发药发药或退药dispensed已发药只能退药或作废returned已退药不允许操作cancelled已作废不允许操作在Django里状态字段用choices定义所有状态变更必须经过模型的方法不让外部直接修改status字段。比如class Prescription(models.Model): STATUS_CHOICES [ (pending, 待审核), (approved, 审核通过), (dispensed, 已发药), (returned, 已退药), (cancelled, 已作废), ] order_no models.CharField(max_length32, uniqueTrue, verbose_name处方单号) doctor models.ForeignKey(User, on_deletemodels.PROTECT, related_nameprescriptions, verbose_name开方医生) patient_name models.CharField(max_length64, verbose_name患者姓名) patient_id models.CharField(max_length32, verbose_name患者编号) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultpending, verbose_name状态) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) def approve(self): if self.status ! pending: raise ValueError(只有待审核处方才能审核通过) self.status approved self.save() def dispense(self): if self.status ! approved: raise ValueError(只有审核通过的处方才能发药) self.status dispensed self.save()为什么这么设计直接改成“允许任意状态跳转”不更简单吗我在设计评审时被问到过这个问题。答案是药房业务经不起状态乱跳。一张还没审核的处方如果被发药了药发出去之后才发现医生开的剂量有问题整个流程就乱套了。状态机把每一步都钉死系统层面就拦截了业务上的违规操作。4.2 扣库存时的并发防线行锁是必需品发药环节最核心的操作是确认发药时扣减对应药品的批次库存。这个操作看起来很简单——从DrugStock里把quantity减掉一个值但并发场景下很容易出问题。想象一下两个窗口的药剂师同时发同一个批次的药两边同时读到quantity是100假设各发10盒两个人都计算出新库存是90然后同时写回数据库。最终结果是90而不是正确的80。这在技术上叫“丢失更新”医药场景下更严重因为会直接导致库存虚高、账实不符。解决方案是用事务加行锁在读取库存行时就锁定它让第二个事务等第一个事务提交后再读取from django.db import transaction from django.db.models import F transaction.atomic def deduct_stock(prescription_item): 扣减库存核心逻辑利用行锁防止并发超卖。 stock DrugStock.objects.select_for_update().get( idprescription_item.stock_id ) if stock.quantity prescription_item.quantity: raise ValueError(当前批次库存不足) stock.quantity F(quantity) - prescription_item.quantity stock.save()select_for_update()在PostgreSQL里会翻译成SELECT FOR UPDATE直接把该行锁定。当两个事务同时扣同一批次时第二个事务会被阻塞等第一个提交后再继续这时读到的是扣减后的最新值。这是库存类系统的标准解法不是我的原创但确实是最有效的常规手段。我还配合用了F表达式来避免读到缓存中的旧值。F表达式会把“quantity F(quantity) - value”翻译成SQL层面的原子减操作进一步降低并发下出错的可能。4.3 批次追溯与“近效期先出”的发药规则药物管理里有个特殊业务规则发药时应该优先发放最早过期的批次也就是“近效期先出”FEFO。如果发药逻辑只是随机挑一个批次扣库存很容易出现某个批次的药一直不被使用最后放过期的情况。要实现FEFO只需要在待发药的批次列表里按expiry_date升序排序依次扣减def get_fefo_stocks(drug_id): return DrugStock.objects.filter( drug_iddrug_id, quantity__gt0, expiry_date__gtedate.today(), is_lockedFalse, ).order_by(expiry_date, batch_no)在发药操作时按这个顺序取批次先扣最早的。如果最早的批次数量不够就继续扣下一个批次直到满足处方数量。整个过程会生成对应的发药明细记录发了哪个批次的药。批次追溯的意义不止在效期管理。一旦某批药品被药监部门通报质量问题系统可以精确查出这批药发给了哪些患者反向联系召回。我当时做测试时模拟过一个场景某批次药品被标记为“问题批次”通过系统检索发药记录几分钟内就拿到了完整的患者名单。这种能力手工台账是完全不具备的。退药处理也值得一提。患者退回药品时不能简单地把数量加回去要判断该批次是否还在有效期内、是否已被锁定。退药需要关联原处方、登记退药原因并生成红字记录。我的处理方式是单独建一张ReturnRecord表退药成功后同时更新库存批次数量和处方状态。这样退药操作和普通入库在审计上是严格区分的。5. 库存预警不靠拍脑袋低存量与效期管理的实现思路5.1 预警阈值的业务规则库存预警的第一步是定义阈值但阈值不能拍脑袋设一个固定值。我从药房实际运营角度总结了两个维度补货预警和断货预警。补货预警的阈值我建议按照“日均消耗量乘以补货周期天数再加安全库存天数”来计算。公式很简单预警阈值 日均消耗量 ×补货周期天数 安全库存天数举个例子某药品日均消耗10盒从下采购单到供应商送货上门平均需要5天药房希望保留3天安全库存那么预警阈值就是10×(53)80盒。低于80盒时系统提示需要补货了。断货预警则可以设为0或者3天用量当库存低于这个值时系统会加急提醒。日均消耗量怎么算不能简单拿“总出库量除以天数”因为门诊量有波动。我的做法是取最近30天的平均出库量如果药品是刚上架的没有历史数据就参考同类药品的消耗量人工预置。预警阈值不是写死的常量而是每个药品可以独立配置的字段药房管理员可以根据季节性和采购周期变化随时调整。5.2 定时任务与实时判断两种方案预警的实现主要有两种方案定时扫描和实时判断。定时扫描最简单直接。每天凌晨用Celery Beat跑一个定时任务遍历所有有效库存批次把近效期和低库存的数据写入预警表。第二天药房打开系统首页时直接展示预警列表。这个方案的优点是逻辑简单、对数据库压力可控适合预警实时性要求不高的场景。我在实际项目里用的就是这种方案。定时任务代码大概长这样from celery import shared_task shared_task def daily_stock_alert(): today date.today() # 近效期预警有效期在90天内的批次 expiring_soon DrugStock.objects.filter( expiry_date__ltetoday timedelta(days90), expiry_date__gttoday, quantity__gt0, ).select_related(drug) for stock in expiring_soon: alert, created StockAlert.objects.get_or_create( alert_typeexpiry, stockstock, defaults{ message: f药品 {stock.drug.generic_name} 批次 {stock.batch_no} 将于 {stock.expiry_date} 到期, }, ) if not created: alert.save()另一种方案是实时判断即发药操作扣减库存后立刻检查该药品当前库存是否已经低于阈值如果低于就当场生成预警。这个方案的实时性好能精确捕捉到“发药后刚好跌破阈值”的时刻但需要对每个药品的阈值做缓存避免频繁查询。两者不冲突。我在项目里是同时用的实时判断负责“当下”的提醒定时任务负责“每日”的汇总与扫描兜底。预警数据统一展示在Dashboard上按紧急程度分颜色红色是断货或过期黄色是近效期或低库存。5.3 Dashboard展示与TOP统计预警只是手段最终要让管理员看得舒服、能辅助决策。我在Dashboard上做了三个区块第一是预警总览。展示当前低库存药品数量、近效期批次数量、已过期且未清理的批次数量。每个数字都可以点击进入对应的明细列表。第二是消耗TOP10。统计最近30天出库量最大的十个药品用列表展示。这个数据对采购计划非常有用消耗排名靠前的药品补货周期可以相应缩短避免断货。第三是库存周转情况的简单汇总。比如库存总品种数、库存总金额估算、近30天入库单数和发药单数。这些数据在周会汇报时可以直接截图省去了手工整理报表的麻烦。统计查询用ORM的聚合函数就够用了from django.db.models import Sum # 最近30天消耗TOP10 top_drugs ( PrescriptionItem.objects.filter( prescription__created_at__gtetimezone.now() - timedelta(days30), prescription__statusdispensed, ) .values(drug_id, drug__generic_name) .annotate(total_quantitySum(quantity)) .order_by(-total_quantity)[:10] )这里有个性能细节统计SQL里的数量字段是Decimal类型Sum的结果类型也是Decimal。做展示时如果涉及金额计算建议全部用Decimal避免混用Float造成精度错误。另外统计查询涉及的表比较多数据量上来之后记得给created_at和status字段加联合索引不然报表接口会越来越慢。6. 权限与安全医生、药师、管理员各管一摊6.1 基于Django Auth的角色体系医院药物管理系统天然就有清晰的用户分层医生开处方但不能审核发药药剂师审核发药但不能开处方系统管理员管基础数据但不介入具体业务单据。权限模型我建议直接用Django自带的Group加Permission。具体做法是预置三个用户组doctor组权限包括创建处方、查看自己开的处方列表。不能查看其他医生的处方详情不能操作库存。pharmacist组权限包括查看所有待审核处方、审核通过、发药、退药、查看库存批次。不能修改药品主档。admin组权限包括药品主档管理、供应商管理、用户管理、查看所有报表、操作盘点。在Django里自定义权限需要先在Model的Meta里声明然后在Group里勾选。比如class Drug(models.Model): # ...字段 class Meta: permissions [ (can_manage_drug, 可以管理药品主档), (can_view_drug, 可以查看药品主档), ]之后用migrate生成权限再在admin后台把权限分配到对应的Group里。整个过程不需要写任何业务视图代码完全靠框架能力解决这是Django做管理系统的红利。6.2 视图层的权限控制模型层的权限定义好了视图层要做的就是执行校验。Django提供了login_required和permission_required这两个装饰器加在视图函数上即可from django.contrib.auth.decorators import login_required, permission_required login_required permission_required(drug.can_manage_drug, raise_exceptionTrue) def drug_create_view(request): # 新增药品 ...对于函数视图装饰器是最简单的。如果是类视图可以用LoginRequiredMixin和PermissionRequiredMixin。我在项目里统一加了raise_exceptionTrue这样权限不足时直接返回403页面而不是把用户弹回登录页——这两种情况用户感知差异很大403说明“你登录了但没权限”跳登录页会让人误以为会话过期了。还有一条红线权限控制绝对不能只靠前端隐藏按钮。用户完全可以绕过前端直接调接口所以后端必须做完整校验。我测试时专门用一个普通药剂师账号手工构造URL去尝试访问药品管理页面确保后端能正确拦截。这种自测方式建议大家都做一遍。6.3 操作日志设计药房系统的审计需求很强我项目里单独设计了一张操作日志表记录所有关键业务操作。class OperationLog(models.Model): user models.ForeignKey(User, on_deletemodels.PROTECT, verbose_name操作人) action models.CharField(max_length32, verbose_name操作类型) target_model models.CharField(max_length64, verbose_name操作对象) target_id models.CharField(max_length32, verbose_name对象ID) detail models.JSONField(defaultdict, verbose_name操作详情) ip_addr models.GenericIPAddressField(blankTrue, nullTrue, verbose_nameIP地址) created_at models.DateTimeField(auto_now_addTrue, verbose_name操作时间) class Meta: db_table operation_log verbose_name 操作日志日志的写入我放在service层统一处理比如发药成功后紧接着写一条“用户X对处方单号Y执行发药操作涉及药品Z批次B数量Q”的日志。不要小看这个细节当系统上线后出现纠纷时这些日志就是唯一的电子证据。我问过一个在医院信息科工作的朋友他给我讲过一个真实场景药房值班人员说某张处方不是他发的登录记录显示却是他的账号。最后查日志发现是保管员把密码贴在显示器上被别人拿去用了。此后医院强制要求每季度改密码同时登录日志和操作日志双留存。这类安全管理经验写进系统设计里就是加分项。7. 实测中遇到的坑从时区到并发扣减的真实教训7.1 一个把库存扣成负数的排查过程开发完成后我做了整整两天的并发测试期间遇到一个非常经典的库存负数问题排查过程值得完整复盘。现象是用脚本模拟两个药剂师同时发同一批次的药偶尔会出现库存被扣成负数。第一次排查时怀疑是代码逻辑问题但看代码走了好几遍扣减逻辑加上判断“库存不足则报错”逻辑上不该出现负数。后来把错误日志翻出来发现发生时刻恰好是两个事务几乎同时进入扣减模块。问题出在我最初写的代码我先查了一次库存数量做判断执行到扣减时又查了一次两个查询之间没有锁于是判断通过后另一个事务已经把库存扣走了当前事务接着扣库存就变负了。修复方案就是前面说的select_for_update()。在同一个事务里先锁行再判断再扣减整个过程是原子的。修复后再跑并发测试连续模拟100组并发扣减库存数量始终精确吻合。所以我的建议是库存扣减类代码从一开始就写好行锁不要等到出负数再补。7.2 金额精度问题为什么不用FloatField另一个让我记忆深刻的坑是金额精度。药品的进价、售价、总价都涉及小数的乘加运算。用FloatField存金额表面上看起来没问题但内部是二进制浮点数0.1加0.2会变成0.30000000000000004。药房月度报表如果基于这种数据做汇总最终金额会和实际发票对不上。解决方案是在所有涉及货币和数量的字段上用Django的DecimalField并且把decimal_places控制在2位以内数量字段我用了3位因为有的针剂按支、有的按人份存在0.5支的情况。在Python层面计算金额时用Decimal包而不是直接拿float做运算。前端展示时如果需要转成浮点只做展示层的格式化不做计算。这套规范必须在数据库设计阶段就定下来否则后期表结构迁移的代价巨大。当时和我协作的数据库工程师说了一句很实在的话钱和药的数量是系统里最不能开玩笑的两个数字。7.3 药品字典清洗与编码兼容医院药物管理系统上线前最大的体力活是整理药品字典。医院给的Excel药品清单鱼龙混杂同一种药有的写“阿莫西林胶囊”有的写“阿莫西林 胶囊”厂家名称也存在简写和全称的差异。我的做法是给每种药品生成一个内部编码规则是“剂型拼音首字母类别号流水号”编码生成后作为主键级别唯一约束所有业务关联都用编码而不是用名称。名称只是展示字段。这个策略在后续对接HIS或医保系统时作用巨大因为外部系统的药品编码体系和我们不一致但内部编码可以作为映射的中间层。清洗药品字典时有个经验不要指望一份Excel就能搞定至少要经过三轮确认。第一轮由信息科按通用名去重第二轮药房主管审核规格和厂家第三轮采购核对批准文号。这个流程虽然耗时但能避免上线后因为药品主档错误导致的连锁问题。7.4 数据校验与测试策略最后分享一下我的测试方法。我没有等全部功能完成才开始测而是每一个核心闭环入库-库存增加、处方-审核-发药-库存减少完成后立刻用手工接口测试工具验证同时准备了一套模拟数据两个供应商、五十种药品、三个医生账号、两个药剂师账号、一个管理员账号。模拟数据里特别构造了一些边界场景近效期药品、过期药品、库存为0的药品、两个批次同一药品但效期不同的场景。这些边界数据能有效检验预警规则和发药逻辑是否正确。测试脚本则模拟多用户并发发药脚本跑完后核对每个批次的库存数量和发药记录里的扣减数量是否一致。药房系统属于医疗信息化范畴上线前的验证怎么仔细都不为过。我当时的验收标准是连续跑一周模拟发药库存账实误差保持为0预警消息无遗漏。达标之后才敢拿去实际环境做试运行。最后再分享一个小技巧如果你也在做类似系统我建议在开发前就先把“药品主档”和“库存批次”两张表的关系想清楚。这两个概念一旦混淆后面所有业务逻辑都会变得拧巴。我在实际开发中最大的感受就是数据模型设计花的时间越多后期写业务代码反而越轻松。只要模型边界清晰订单流转、库存预警、权限隔离这些功能做起来其实都是水到渠成的事。试运行期间药房阿姨说系统比台账好用多了这话比什么验收报告都让人踏实。