医院药物管理系统是医疗机构日常运营中不可或缺的核心业务系统。传统的人工管理方式存在诸多痛点:药品库存更新滞后、效期管理依赖人工检查、处方流转效率低下、数据统计分析困难等。这些问题直接影响到患者的用药安全和医院的运营效率。
我们团队基于Django框架开发的这套新型药物管理系统,正是为了解决这些行业痛点而生。系统采用B/S架构设计,前端使用Bootstrap实现响应式布局,后端基于Python+Django+MySQL技术栈,实现了药品全生命周期的数字化管理。在实际部署的某三甲医院试点中,系统将药品管理差错率降低了40%,库存周转效率提升35%,处方处理时间缩短50%。
选择Django作为后端框架主要基于以下考量:
数据库选用MySQL 5.7版本,主要考虑:
系统采用典型的三层架构设计:
code复制表示层(Bootstrap+Vue.js)
↑↓ HTTP/HTTPS
业务逻辑层(Django+Redis缓存)
↑↓ ORM
数据存储层(MySQL+文件存储)
关键设计决策:
药品基础信息采用E-R模型设计:
python复制class Medicine(models.Model):
# 药品唯一标识
medicine_id = models.CharField(max_length=20, unique=True)
# 通用名
generic_name = models.CharField(max_length=100)
# 商品名
brand_name = models.CharField(max_length=100)
# 规格
specification = models.CharField(max_length=50)
# 剂型(片剂/胶囊/注射液等)
dosage_form = models.CharField(max_length=20)
# 生产厂家
manufacturer = models.ForeignKey('Manufacturer', on_delete=models.PROTECT)
# 批准文号
approval_number = models.CharField(max_length=50)
# 药品图片
image = models.ImageField(upload_to='medicines/')
# 分类(处方药/非处方药)
category = models.CharField(max_length=10, choices=CATEGORY_CHOICES)
# 单价(精确到分)
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
# 库存预警阈值
stock_threshold = models.PositiveIntegerField(default=100)
class Meta:
indexes = [
models.Index(fields=['generic_name']),
models.Index(fields=['category']),
]
关键设计要点:
- 使用ImageField处理药品图片,配合Pillow库实现图片压缩
- 为高频查询字段添加数据库索引
- 价格字段使用Decimal避免浮点数精度问题
- 建立与生产厂家的外键关联,确保数据一致性
库存管理模块的核心算法:
python复制def calculate_replenishment(medicine_id):
"""
智能补货计算算法
参数:medicine_id 药品ID
返回:建议补货数量
"""
medicine = Medicine.objects.get(pk=medicine_id)
# 获取近30天消耗量
consumption = DispenseDetail.objects.filter(
medicine=medicine,
dispense_time__gte=timezone.now()-timedelta(days=30)
).aggregate(total=Sum('quantity'))['total'] or 0
# 获取当前库存
current_stock = Stock.objects.get(medicine=medicine).quantity
# 计算安全库存(近30天平均消耗量×采购周期×安全系数)
avg_daily_consumption = consumption / 30
lead_time = 7 # 假设采购周期为7天
safety_factor = 1.2 # 安全系数
safety_stock = avg_daily_consumption * lead_time * safety_factor
# 建议补货量 = 安全库存 - 当前库存 + 预计消耗量
suggested_qty = safety_stock - current_stock + (avg_daily_consumption * lead_time)
return max(0, round(suggested_qty))
库存预警实现方案:
python复制@receiver(post_save, sender=Stock)
def check_stock_level(sender, instance, **kwargs):
if instance.quantity < instance.medicine.stock_threshold:
notify_low_stock.delay(instance.medicine_id)
@periodic_task(run_every=crontab(hour=8, minute=0))
def check_expiry():
today = timezone.now().date()
threshold_date = today + timedelta(days=30) # 提前30天预警
expiring = Batch.objects.filter(
expiry_date__lte=threshold_date,
expiry_date__gte=today,
is_notified=False
)
for batch in expiring:
notify_expiry.delay(batch.id)
batch.is_notified = True
batch.save()
处方处理状态机设计:
python复制class Prescription(models.Model):
STATUS_CHOICES = [
('draft', '草稿'),
('submitted', '已提交'),
('pharmacist_review', '药师审核'),
('dispensed', '已发药'),
('rejected', '已退回'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft'
)
def transition_to(self, new_status, by_user):
"""状态转移逻辑"""
transitions = {
'draft': ['submitted'],
'submitted': ['pharmacist_review', 'rejected'],
'pharmacist_review': ['dispensed', 'rejected'],
'dispensed': [],
'rejected': ['submitted'],
}
if new_status not in transitions.get(self.status, []):
raise InvalidStatusTransition(
f"Cannot transition from {self.status} to {new_status}"
)
# 记录操作日志
PrescriptionLog.objects.create(
prescription=self,
from_status=self.status,
to_status=new_status,
operator=by_user
)
self.status = new_status
self.save()
药物相互作用检查实现:
python复制def check_interactions(medicine_ids):
"""
检查药物相互作用
参数:medicine_ids 药品ID列表
返回:相互作用详情列表
"""
interactions = []
# 从药品知识库获取相互作用规则
rules = InteractionRule.objects.filter(
models.Q(medicine_a__in=medicine_ids) |
models.Q(medicine_b__in=medicine_ids)
).distinct()
for rule in rules:
if rule.medicine_a_id in medicine_ids and rule.medicine_b_id in medicine_ids:
interactions.append({
'medicine_a': rule.medicine_a.generic_name,
'medicine_b': rule.medicine_b.generic_name,
'severity': rule.get_severity_display(),
'description': rule.description
})
return interactions
python复制# models.py
class Role(models.Model):
name = models.CharField(max_length=50, unique=True)
permissions = models.ManyToManyField('Permission')
class Permission(models.Model):
codename = models.CharField(max_length=100)
name = models.CharField(max_length=100)
class User(AbstractUser):
roles = models.ManyToManyField(Role)
def has_perm(self, perm_codename):
return self.roles.filter(
permissions__codename=perm_codename
).exists()
# 权限装饰器
def permission_required(perm_codename):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.user.has_perm(perm_codename):
raise PermissionDenied
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
典型权限分配:
python复制class AuditLog(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
action = models.CharField(max_length=100) # 操作类型
object_type = models.CharField(max_length=50) # 操作对象类型
object_id = models.CharField(max_length=50) # 操作对象ID
ip_address = models.GenericIPAddressField()
timestamp = models.DateTimeField(auto_now_add=True)
details = models.JSONField() # 操作详情
@transaction.atomic保证数据一致性django-cryptography加密存储python复制# 反例:N+1查询问题
prescriptions = Prescription.objects.all()
for p in prescriptions:
print(p.patient.name) # 每次循环都查询数据库
# 正例:使用select_related
prescriptions = Prescription.objects.select_related('patient').all()
for p in prescriptions:
print(p.patient.name) # 预先获取关联数据
explain分析慢查询python复制from django.core.cache import cache
def get_medicine_detail(medicine_id):
cache_key = f'medicine_{medicine_id}'
data = cache.get(cache_key)
if not data:
data = Medicine.objects.filter(pk=medicine_id).values(
'generic_name', 'brand_name', 'specification'
).first()
cache.set(cache_key, data, timeout=3600) # 缓存1小时
return data
python复制CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PICKLE_VERSION': -1, # 使用最高协议版本
'SOCKET_CONNECT_TIMEOUT': 5, # 秒
'SOCKET_TIMEOUT': 5, # 秒
}
}
}
code复制 +-----------------+
| Nginx |
| (负载均衡+SSL) |
+--------+--------+
|
+----------------+----------------+
| | |
+------+------+ +------+------+ +------+------+
| uWSGI | | uWSGI | | uWSGI |
| Worker1 | | Worker2 | | Worker3 |
+------+------+ +------+------+ +------+------+
| | |
+------+----------------+----------------+------+
| Redis |
| (缓存/消息队列) |
+------+--------------------------------+------+
| |
+------+------+ +------+------+
| MySQL | | MySQL |
| Master | | Slave |
+-------------+ +-------------+
uWSGI配置示例(uwsgi.ini):
ini复制[uwsgi]
chdir = /opt/pharmacy-system
module = pharmacy.wsgi:application
master = true
processes = 4
threads = 2
vacuum = true
max-requests = 1000
harakiri = 30
socket = /tmp/pharmacy.sock
chmod-socket = 666
stats = /tmp/stats.socket
Nginx关键配置:
nginx复制upstream pharmacy {
server unix:///tmp/pharmacy.sock;
}
server {
listen 443 ssl;
server_name pharmacy.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
include uwsgi_params;
uwsgi_pass pharmacy;
uwsgi_read_timeout 300;
}
location /static/ {
alias /opt/pharmacy-system/static/;
expires 30d;
}
location /media/ {
alias /opt/pharmacy-system/media/;
expires 30d;
}
}
pre_save信号自动生成编码python复制@receiver(pre_save, sender=Medicine)
def generate_medicine_id(sender, instance, **kwargs):
if not instance.medicine_id:
# 获取分类码和剂型码
category_code = MEDICINE_CATEGORY_CODES[instance.category]
form_code = DOSAGE_FORM_CODES[instance.dosage_form]
# 获取最大流水号
max_seq = Medicine.objects.filter(
category=instance.category,
dosage_form=instance.dosage_form
).count()
instance.medicine_id = f"{category_code}{form_code}{max_seq+1:05d}"
django-import-export处理Excel导入bulk_create减少数据库查询python复制def import_medicines(file_path):
batch_size = 100
medicines = []
with open(file_path, 'rb') as f:
workbook = load_workbook(f)
sheet = workbook.active
for row in sheet.iter_rows(min_row=2, values_only=True):
medicine = Medicine(
generic_name=row[0],
brand_name=row[1],
specification=row[2],
# 其他字段...
)
medicines.append(medicine)
if len(medicines) >= batch_size:
Medicine.objects.bulk_create(medicines)
medicines = []
if medicines:
Medicine.objects.bulk_create(medicines)
python复制from django.db import transaction
@transaction.atomic
def dispense_medicine(medicine_id, quantity):
stock = Stock.objects.select_for_update().get(medicine_id=medicine_id)
if stock.quantity >= quantity:
stock.quantity -= quantity
stock.save()
return True
return False
python复制# 每次循环都执行查询
batches = Batch.objects.filter(expiry_date__gte=now)
for b in batches:
print(b.medicine.generic_name) # N+1问题
python复制# 使用select_related一次性获取关联数据
batches = Batch.objects.select_related('medicine').filter(expiry_date__gte=now)
for b in batches:
print(b.medicine.generic_name) # 无额外查询
python复制class MedicineAdmin(admin.ModelAdmin):
list_display = ('medicine_id', 'generic_name', 'category', 'current_stock')
list_filter = ('category', 'dosage_form')
search_fields = ('generic_name', 'brand_name')
readonly_fields = ('medicine_id',)
autocomplete_fields = ('manufacturer',)
def current_stock(self, obj):
return obj.stock.quantity if hasattr(obj, 'stock') else 0
current_stock.short_description = '当前库存'
在实际部署过程中,我们发现药品效期管理模块需要特别关注。最初版本仅提供静态预警,后来改进为动态预警算法,综合考虑了药品使用频率、供应商交货周期等因素,使得预警更加精准。这个改进使得近效期药品报废率降低了60%,显著减少了药品浪费。