做了这么多年Web开发,接过的管理系统不说一百也有八十个,但医药信息管理这个方向,始终是里面比较特殊的一类。它不像普通的进销存,管好库存和订单就行,医药行业本身带着天然的严谨属性——药品批次、生产日期、有效期、供应商资质、库存预警,每一项都直接关系到用药安全。早几年我用PHP搭过一套,后来用Spring Boot重构过一版,但如果你现在问我用什么最快、最稳地交付一套医药信息管理系统,我的答案一定是Python + Django,没有之一。
这个结论不是拍脑袋。Django自带的后台管理(Admin)、ORM、认证体系、表单处理,几乎就是为“信息管理系统”这个场景量身定做的。你不需要从零搭建用户登录、权限控制、数据库连接这些地基工程,可以集中精力去处理医药业务本身的核心逻辑。这篇文章,我就以“基于Python + Django医药信息管理系统(源码+数据库+文档)”为主线,把我从零搭建这套系统的完整思路、表结构设计、核心模块实现和部署踩坑经验全部拆开讲清楚。
这套系统适合谁参考?两类人。一类是计算机专业的同学,做课程设计或者毕业设计时,选“医药信息管理”这个题目很常见,这篇文章可以帮你理清设计思路,避开“为了交差而堆功能”的大坑;另一类是刚入门Django的开发者,想找一个完整的、带数据库设计和文档的实战项目来练手,这篇文章能帮你建立“从需求到上线”的整体认知。无论你是哪一类,我希望你读完之后,不光是会抄代码,而是真正理解这套系统为什么要这么设计,每一步背后是什么逻辑。
1. 为什么偏偏是Django:医药信息管理系统的选型逻辑
很多人选型的时候容易被“技术栈热度”带跑,一会儿觉得Spring Boot高大上,一会儿觉得前后端分离React + Node.js才是未来。但对医药信息管理系统这种典型的“CRUD密集型”项目,我的观点一直很明确:用最合适的工具,而不是最炫的工具。
1.1 Django Admin:你最容易忽视的“核武器”
Django最被低估的功能就是它的Admin后台。医药信息管理系统里,药品目录管理、供应商信息维护、入库单审核、库存调整记录——这些功能本质上是给内部管理员使用的操作界面。你如果用Django开发,Django Admin几乎不需要写一行前端代码,就能把完整的增删改查界面给你生成出来。
我见过很多初学者在Admin上栽跟头,觉得它“太丑”“太简陋”,非要自己写一套Vue + Element UI的前端。结果呢?光是一个表格分页、搜索过滤、表单验证,就得花掉两三天时间,而且做出来的效果还不一定有Django Admin好用。Admin自带分页、搜索、过滤器、字段排序、批量操作、权限控制,你花5分钟注册一下Model进去,它就全有了。
实际项目里我通常的做法是:先用Django Admin把后台管理功能全部跑通,交给客户或老师验收核心业务流程。如果后续确实需要定制化的前端界面,再逐步用Django Template或者单独写API对接。
这套思路的好处是:系统的骨架永远不会变,变的只是“皮肤”层。 医药信息管理系统的核心是数据模型和业务逻辑,这两部分在Django里都写在后端,前端再怎么换,后端稳如泰山。
1.2 ORM带来的数据层安全感
医药数据的准确性是系统生命线。手工写SQL拼接字符串,稍不注意就会出SQL注入漏洞;手写数据库连接管理,一不小心就连接泄漏。Django的ORM把这些问题全部挡在门外。
ORM(对象关系映射)用大白话说就是:你不直接跟数据库表打交道,而是通过Python类来操作数据。 比如你要查所有库存低于预警值的药品,用Django ORM写就是一行:
python复制low_stock_drugs = Drug.objects.filter(stock_quantity__lt=F('low_stock_threshold'))
再比如你想统计每月入库总额:
python复制from django.db.models import Sum
monthly_total = StockInRecord.objects.filter(
created_at__month=current_month
).aggregate(total=Sum('total_price'))
这种写法,第一不会出现SQL注入风险,因为ORM底层已经做了参数化处理;第二代码可读性极高,后期维护成本低;第三,Django自动帮你在数据库层面做了事务管理,多条数据同时写入时,要么全部成功,要么全部回滚,这一点对医药库存管理尤其重要——不能出现“入库单保存了,但库存数没加上”这种数据不一致的事故。
1.3 自带的后台管理生态
除了Admin,Django还自带了一整套Web开发必备的“基础设施”:用户认证系统(登录、注册、密码加密)、CSRF防护、XSS防护、Session管理、表单校验、分页、消息提示。这些在别的框架里可能要一个个装第三方库,在Django里全是开箱即用。
我做个简单的对比你就明白了:
| 功能模块 | Spring Boot方案 | Flask方案 | Django方案 |
|---|---|---|---|
| 用户认证 | Spring Security(配置复杂) | Flask-Login + 自己实现 | 自带,一行配置 |
| 后台管理 | 手写或接x-admin | 手写 | 自带Admin |
| 数据库迁移 | Flyway/Liquibase | Flask-Migrate | 自带Migrations |
| 表单验证 | 手写或接Hibernate Validator | 手写 | 自带Forms |
| 权限控制 | Shiro/Spring Security | 手写装饰器 | 自带Group/Permission |
从表里你能明显看到,Django在“管理系统”这个赛道上,几乎是零短板的存在。别再纠结“哪个框架更流行”了——选型的关键是看你的项目类型跟框架的优势领域是否匹配。 做电商网站你可能要考虑高并发、秒杀,Django不一定是最优解;但做医药信息管理系统这种典型的业务系统,Django就是效率最高的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统核心拆解:数据表设计与业务模块规划
选定了技术栈,接下来就是最烧脑的部分——业务建模。医药信息管理系统不是简单地把药品信息存进去就完事,它背后暗合着医药流通的完整链条:药品从哪来(供应商)、放在哪(仓库)、谁在管(操作员)、库存够不够(预警)、要往哪去(出库/销售)。数据表设计如果一开始没想清楚,后面改起来简直是灾难。
2.1 从业务到数据表:六张核心表的设计思路
我习惯先画业务流程图,再转成数据表。这套系统的核心业务线其实就两条:药品维护线和进销存流转线。围绕这两条线,我拆出了以下核心表结构:
药品信息表(Drug)
- 药品编号、药品名称、通用名、规格、剂型、生产厂家、批准文号、计量单位
- 库存数量、预警阈值、采购价格、销售价格、有效期至
- 创建时间、更新时间
这张表是整个系统的心脏,所有其他表基本都要跟它关联。药品编号我建议用唯一的字符串,比如国药准字号或者自定义编码,不方便用自增ID,因为这个编号在系统外部也要用(比如贴条码、Excel导入导出)。
供应商信息表(Supplier)
- 供应商编号、公司名称、联系人、联系电话、地址
- 经营范围、资质证号、备注
医药行业的供应商跟普通商品供应商不一样,资质是硬门槛。经营范围、资质证号这些字段必须留足长度,最好支持上传证照图片的附件链接,后续可以扩展“资质到期预警”功能。
入库记录表(StockInRecord)
- 入库单号、关联供应商、入库日期、操作员、入库药品明细、总金额、备注
入库单通常会关联多条药品明细,所以我会拆成“主表 + 从表”的结构:主表存一张入库单的公共信息(单号、日期、供应商、操作员),从表存这条入库单里每样药品入了多少、单价多少。
出库记录表(StockOutRecord)
- 出库单号、出库类型(销售/领用/报损)、出库日期、操作员、出库药品明细、总金额、备注
出库类型这个字段很关键,以后统计报表要按类型分组,这里存成数字或短字符串都可以,但一定要提前定好枚举值。
库存表(Inventory)
- 关联药品、当前库存量、锁定库存量、库位编号、最近入库时间、最近出库时间
你可能要问,药品信息表里不是已经有库存数量了吗?为什么还要单独一张库存表?这就是我踩过坑换来的经验——药品信息表里存的是“基础信息”,库存表存的是“动态数据”。 按道理库存也可以放药品表里,但一旦你要查“某批药品的出入库流水”,或者要按库位盘点,分离出来就方便得多。另外,库存表还可以记录批次号、生产日期、到期日期,这两个字段在医药行业极其重要——药品是有有效期的,单纯存一个库存总数,会导致“把快到期的药先卖出去”这种管理盲区。
出入库明细表(StockTransactionDetail)
- 关联单据、关联药品、数量、单价、批次号、生产日期、有效期至
这张表是“流水账”,只增不改。任何一笔入库、出库、报损,都往这里写一条明细。这样设计的好处是:系统可以随时追溯任何一个药品的完整生命周期——什么时候进的、进了多少、什么时候出的、还剩多少。审计、追溯、防过期,全靠这张表撑着。
用户与操作日志表(User + OperationLog)
- 用户表:用户名、密码(Django默认加密)、姓名、角色、联系电话
- 操作日志:操作员、操作类型、操作时间、操作内容、IP地址
操作日志表是我强烈建议一定要加的,哪怕客户没提需求。医药系统一旦出问题,追责是必须的,没有日志系统保护自己,出了事你百口莫辩。Django的Admin本身会记录操作日志(LogEntry),但你自己的业务操作(比如删除了一条入库单)也要写进日志,别嫌麻烦。
2.2 药品管理模块:不只是增删改查
药品管理模块是系统的基础模块,表面上看起来就是维护一张药品表,但有几个细节值得注意。
多条件组合检索。 药品数量上来之后,光靠翻页找药肯定不行。我做的检索条件是:药品编号模糊匹配、药品名称模糊匹配、生产厂家下拉选择、库存状态筛选(有货/无货/预警)。Django里查这个不要写一堆if去拼QuerySet,可以用Q对象优雅实现:
python复制from django.db.models import Q
def drug_list(request):
keyword = request.GET.get('keyword', '')
manufacturer = request.GET.get('manufacturer', '')
stock_status = request.GET.get('stock_status', '')
queryset = Drug.objects.all()
if keyword:
queryset = queryset.filter(
Q(drug_code__icontains=keyword) |
Q(drug_name__icontains=keyword) |
Q(generic_name__icontains=keyword)
)
if manufacturer:
queryset = queryset.filter(manufacturer=manufacturer)
if stock_status == 'out':
queryset = queryset.filter(stock_quantity=0)
elif stock_status == 'warning':
queryset = queryset.filter(
stock_quantity__gt=0,
stock_quantity__lte=F('low_stock_threshold')
)
elif stock_status == 'normal':
queryset = queryset.filter(
stock_quantity__gt=F('low_stock_threshold')
)
这段代码看起来平平无奇,但它解决了一个很实际的问题:查询条件任意组合,不会因为某个条件为空就出bug。 很多新手写这种搜索,喜欢每查一个条件就重写一次数据库查询,或者把所有条件串进一个字符串SQL里,既难读又危险。
有效期预警。 医药药品不能只管库存够不够,还得管“这批药还能不能卖”。我在药品列表页显示两个预警状态:库存预警(库存低于阈值)和效期预警(距离有效期不足90天)。效期预警的实现也不复杂,在查询时用annotate计算剩余天数,标注在列表中:
python复制from django.utils import timezone
from datetime import timedelta
from django.db.models import F, ExpressionWrapper, DurationField
ninety_days_later = timezone.now().date() + timedelta(days=90)
expiring_drugs = Drug.objects.filter(
expiry_date__lte=ninety_days_later,
expiry_date__gte=timezone.now().date()
)
这个功能在课程设计答辩时非常加分——评委一看就知道你考虑了医药行业的真实业务场景,而不是纯粹在写“数据库课设”。
2.3 进货入库与销售出库:事务安全是第一原则
入库出库这两个模块,业务逻辑不算复杂,但数据一致性是重中之重。一次入库操作,至少要做三件事:
- 在入库记录表中创建一条主记录和对应明细记录
- 增加该药品的库存数量
- 在出入库明细表中写入流水
这三件事是一个整体,必须放在同一个数据库事务里,任何一步失败,全部回滚。Django里用transaction.atomic()装饰器或上下文管理器就能轻松实现:
python复制from django.db import transaction
from django.utils import timezone
@transaction.atomic
def create_stock_in(stock_in_data, items_data):
# 1. 创建入库主表记录
stock_in = StockInRecord.objects.create(
stock_in_no=generate_stock_in_no(),
supplier_id=stock_in_data['supplier_id'],
operator=stock_in_data['operator'],
stock_in_date=timezone.now(),
total_amount=sum(item['subtotal'] for item in items_data),
remark=stock_in_data.get('remark', '')
)
# 2. 创建入库明细并更新库存
for item in items_data:
StockInItem.objects.create(
stock_in=stock_in,
drug_id=item['drug_id'],
quantity=item['quantity'],
purchase_price=item['price'],
total_price=item['quantity'] * item['price']
)
# 更新药品库存(如果有批次则更新对应批次库存)
drug = Drug.objects.select_for_update().get(pk=item['drug_id'])
drug.stock_quantity += item['quantity']
drug.save()
# 3. 写入操作日志
OperationLog.objects.create(
operator=stock_in_data['operator'],
operation_type='stock_in',
operation_content=f'药品入库:{stock_in.stock_in_no}',
operation_time=timezone.now()
)
return stock_in
我重点解释一下select_for_update()这行。它是对数据库行加悲观锁,防止两个人同时操作同一个药品的库存导致数据错乱。 比如A操作员和B操作员同时给“阿莫西林”入库,如果不加锁,两个人同时读到库存是100,各加50,最后库存可能被写成了150而不是200。加上行锁之后,A先操作,B必须等A提交事务后才能继续。这种并发问题在校园网课设里很难暴露,但真到了实际运营环境,早晚会翻车。
出库的逻辑跟入库完全对称,多一个负库存校验:
python复制if drug.stock_quantity < item['quantity']:
raise ValueError(f"药品 {drug.drug_name} 库存不足,当前库存仅剩 {drug.stock_quantity},无法出库 {item['quantity']}。")
这个校验必须在事务里面做,还要用select_for_update()锁行,否则“两个人同时看到有库存,但实际总共只够一个人买”的问题分分钟复现。
3. 从零到可运行:项目搭建与核心代码实现
这一部分我按“实操步骤”来写,你用一台干净的电脑也能跟着走完。考虑到不少读者在Windows上开发,我会把命令写成跨平台的通用形式。如果你用的是Mac或Linux,命令基本一样,只有虚拟环境的激活命令略有区别。
3.1 环境准备:别在第一步偷懒
我见过太多人第一步就卡住——Python装了多个版本,Django装在了全局环境里,项目跑到一半发现版本冲突,最后只能重来。第一步就建虚拟环境,永远是正确选择。
bash复制# 1. 创建项目目录并进入
mkdir medical_info_system
cd medical_info_system
# 2. 创建虚拟环境(Windows)
python -m venv venv
venv\Scripts\activate
# 创建虚拟环境(Mac/Linux)
# python3 -m venv venv
# source venv/bin/activate
# 3. 安装Django(建议使用LTS版本)
pip install django==4.2.*
# 4. 创建Django项目和应用
django-admin startproject medical_system .
python manage.py startapp drug_manage
python manage.py startapp stock_manage
python manage.py startapp user_manage
解释一下这个项目结构的设计意图。drug_manage管药品和分类信息,stock_manage管入库出库和库存查询,user_manage管用户和操作日志。按业务模块拆app,而不是一个app塞所有东西,这是Django大型项目的标准做法。一个app塞几百个Model和View的“巨石应用”,后期维护绝对崩溃。
虚拟环境的原理我用个生活类比:它相当于给每个项目一个独立的房间,房间里的工具、依赖互不干扰。 你的A项目用Django 3.2,B项目用Django 5.0,各住各的房间,互不打架。不建虚拟环境的后果就是,所有项目共用一个“大仓库”,A项目升级Django版本,B项目当场挂掉。
3.2 路由规划:从URL设计看系统全貌
Django的路由(URL配置)设计得好不好,直接影响系统可维护性。我的习惯是先设计一套整洁的URL,再回头写view。这套系统的路由规划如下:
| URL路径 | 功能模块 | 说明 |
|---|---|---|
/accounts/login/ |
用户认证 | Django自带登录视图 |
/admin/ |
后台管理 | Django Admin入口 |
/drug/list |
药品管理 | 药品列表 |
/drug/add |
药品管理 | 新增药品 |
/drug/edit/<int:pk> |
药品管理 | 编辑指定药品 |
/drug/delete/<int:pk> |
药品管理 | 删除指定药品 |
/supplier/list |
供应商管理 | 供应商列表 |
/stock/in/list |
入库管理 | 入库单列表 |
/stock/in/add |
入库管理 | 新增入库单 |
/stock/out/list |
出库管理 | 出库单列表 |
/stock/out/add |
出库管理 | 新增出库单 |
/stock/inventory |
库存查询 | 当前库存与预警 |
这套URL设计下来,整个系统哪些功能一目了然。写路由配置的时候,注意用app_name做命名空间,避免多个app之间路由重名:
python复制# medical_system/urls.py 主路由
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),
path('drug/', include('drug_manage.urls')),
path('supplier/', include('drug_manage.supplier_urls')),
path('stock/', include('stock_manage.urls')),
path('report/', include('stock_manage.report_urls')),
]
# drug_manage/urls.py 子路由
from django.urls import path
from . import views
app_name = 'drug'
urlpatterns = [
path('list', views.DrugListView.as_view(), name='drug_list'),
path('add', views.DrugCreateView.as_view(), name='drug_add'),
path('edit/<int:pk>', views.DrugUpdateView.as_view(), name='drug_edit'),
path('delete/<int:pk>', views.DrugDeleteView.as_view(), name='drug_delete'),
]
3.3 关键模型的Model定义:基础字段与索引设计
直接上干货。药品表建议的Model定义,包含关键的字段约束和Meta配置:
python复制from django.db import models
from django.utils import timezone
class Drug(models.Model):
DRUG_TYPE_CHOICES = [
('western', '西药'),
('chinese', '中成药'),
('medical', '医疗器械'),
]
drug_code = models.CharField('药品编码', max_length=32, unique=True)
drug_name = models.CharField('药品名称', max_length=128)
generic_name = models.CharField('通用名', max_length=128, blank=True)
drug_type = models.CharField('药品类型', max_length=32, choices=DRUG_TYPE_CHOICES, default='western')
specification = models.CharField('规格', max_length=64)
dosage_form = models.CharField('剂型', max_length=64, blank=True)
manufacturer = models.CharField('生产厂家', max_length=128)
approval_number = models.CharField('批准文号', max_length=64)
unit = models.CharField('计量单位', max_length=16, default='盒')
stock_quantity = models.IntegerField('当前库存', default=0)
low_stock_threshold = models.IntegerField('库存预警阈值', default=10)
purchase_price = models.DecimalField('采购价', max_digits=10, decimal_places=2)
sale_price = models.DecimalField('销售价', max_digits=10, decimal_places=2)
production_date = models.DateField('生产日期', null=True, blank=True)
expiry_date = models.DateField('有效期至', null=True, blank=True)
description = models.TextField('备注', blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'drug'
ordering = ['-updated_at']
indexes = [
models.Index(fields=['drug_code']),
models.Index(fields=['drug_name']),
models.Index(fields=['manufacturer']),
]
def __str__(self):
return f'{self.drug_name}({self.drug_code})'
@property
def is_low_stock(self):
return self.stock_quantity <= self.low_stock_threshold
@property
def is_expiring(self):
if not self.expiry_date:
return False
return timezone.now().date() <= self.expiry_date <= timezone.now().date() + timedelta(days=90)
几个细节解释一下:
drug_code加unique=True,是业务层面的唯一标识,不允许重复录入同一种药品。ordering = ['-updated_at'],让列表展示默认按最近更新排序,符合“最近修改的药品大概率是正在处理的”这个操作习惯。indexes字段,对于频繁查询的字段加数据库索引,药品列表页模糊搜索会快很多。小数据量看不出来,几千条以后差距就很明显了。is_low_stock和is_expiring写成@property,后面在模板里直接调用drug.is_low_stock就能判断,非常优雅。
3.4 基于类的视图(CBV):写更少代码,做更多事
Django有两种视图写法:函数视图(FBV)和类视图(CBV)。对这个项目,能用CBV的地方我都用CBV,内置的ListView、CreateView、UpdateView、DeleteView基本覆盖了常规CRUD。
以药品列表页为例,用ListView实现:
python复制from django.views.generic import ListView
from django.db.models import Q
from .models import Drug
class DrugListView(ListView):
model = Drug
template_name = 'drug_manage/drug_list.html'
context_object_name = 'drugs'
paginate_by = 15
def get_queryset(self):
queryset = super().get_queryset()
keyword = self.request.GET.get('keyword', '')
if keyword:
queryset = queryset.filter(
Q(drug_code__icontains=keyword) |
Q(drug_name__icontains=keyword)
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['keyword'] = self.request.GET.get('keyword', '')
return context
这段代码实现了:查询所有药品、支持关键字过滤、自动分页(每页15条)、把搜索关键字传回模板用于回显。如果用手写函数视图,这至少要多写20行重复代码。 CBV的价值就是把这些通用逻辑封装好,让你只关心业务差异部分。
新增和编辑药品用CreateView和UpdateView更简单,只需要定义表单和模板:
python复制from django.views.generic import CreateView, UpdateView
from django.urls import reverse_lazy
from .forms import DrugForm
class DrugCreateView(CreateView):
model = Drug
form_class = DrugForm
template_name = 'drug_manage/drug_form.html'
success_url = reverse_lazy('drug:drug_list')
class DrugUpdateView(UpdateView):
model = Drug
form_class = DrugForm
template_name = 'drug_manage/drug_form.html'
success_url = reverse_lazy('drug:drug_list')
你可能会担心CBV太难理解,其实熟练掌握几个常见视图就行,它不是黑魔法。你可以把ListView理解为“已经给你调好一半的列表页面”,你只需要告诉它“查哪个Model、用哪个模板”,剩下的分页、查空、模板取数规则,它全处理好了。
3.5 登录与权限:保护系统的每一层
医药信息管理系统绝对不能裸奔,任何操作都必须有身份记录。Django自带的认证系统已经完整解决了登录、登出、密码加密的问题,我们只需要做两件事:配置登录认证,设置访问权限。
先在settings.py里配置登录跳转:
python复制LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/drug/list'
LOGOUT_REDIRECT_URL = '/accounts/login/'
然后给需要登录才能访问的视图加上@login_required装饰器(函数视图)或LoginRequiredMixin(类视图):
python复制from django.contrib.auth.mixins import LoginRequiredMixin
class DrugListView(LoginRequiredMixin, ListView):
# ...
不加这层保护的话,任何人直接访问/drug/list就能看到全部药品数据,这在医药行业等于把自己的后台数据裸奔到公网上,属于严重安全事故。
权限控制更进一步,可以给不同角色分配不同权限。Django自带的Group和Permission机制可以做到:操作员角色只能做入库、出库,不能删药品数据;管理员角色拥有全部权限。这个在Admin后台的“组和权限”界面里就能配置,不需要额外写代码。
4. 模板渲染与前端交互:让系统真正“能用”
很多Django项目代码写得很漂亮,前端一塌糊涂,表格挤成一团、按钮位置乱飞、弹窗基本靠浏览器默认。这套医药系统的界面呈现,我的原则是:不炫技,但必须清晰。 医药系统使用者的年龄跨度很大,操作界面必须一眼就能看懂。
4.1 继承模板:一次布局,全局复用
Django模板的继承机制大幅减少了前端工作量。我建一个base.html作为全局骨架,放侧边导航栏、顶部状态栏、消息提示区域,所有页面模板通过{% extends 'base.html' %}来继承,只需要写自己这块的内容块:
html复制<!-- templates/base.html 核心骨架 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}医药信息管理系统{% endblock %}</title>
<link href="https://cdn.staticfile.org/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="#">医药信息管理系统</a>
<span class="navbar-text text-white">
当前用户:{{ request.user.username }}
<a href="{% url 'logout' %}" class="text-white ms-2">退出登录</a>
</span>
</div>
</nav>
<div class="container-fluid mt-3">
<div class="row">
<div class="col-md-2">
{% include 'side_menu.html' %}
</div>
<div class="col-md-10">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
模板继承最核心的好处:改一个布局,全站同步生效。 如果有一天你觉得侧边栏宽度不够,只需改base.html一行,所有页面的布局都跟着变。这也是为什么我一直强调“系统骨架”的概念——基础打好了,后面变化一点都不可怕。
4.2 药品列表页:表格+搜索+操作按钮
药品列表页是系统的门面,我用Bootstrap 5搭建,兼顾桌面和移动端显示。关键代码思路如下:
html复制<!-- templates/drug_manage/drug_list.html -->
{% extends 'base.html' %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h4>药品列表</h4>
<a href="{% url 'drug:drug_add' %}" class="btn btn-primary">新增药品</a>
</div>
<form method="get" class="row g-3 mb-3">
<div class="col-auto">
<input type="text" name="keyword" value="{{ keyword }}" class="form-control" placeholder="药品编号/名称">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">搜索</button>
</div>
</form>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>药品编码</th>
<th>药品名称</th>
<th>规格</th>
<th>生产厂家</th>
<th>当前库存</th>
<th>库存状态</th>
<th>有效期至</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for drug in drugs %}
<tr>
<td>{{ drug.drug_code }}</td>
<td>{{ drug.drug_name }}</td>
<td>{{ drug.specification }}</td>
<td>{{ drug.manufacturer }}</td>
<td>{{ drug.stock_quantity }} {{ drug.unit }}</td>
<td>
{% if drug.stock_quantity <= 0 %}
<span class="badge bg-danger">无货</span>
{% elif drug.is_low_stock %}
<span class="badge bg-warning text-dark">库存预警</span>
{% else %}
<span class="badge bg-success">正常</span>
{% endif %}
</td>
<td>
{{ drug.expiry_date|date:"Y-m-d" }}
{% if drug.is_expiring %}
<span class="badge bg-danger">临期</span>
{% endif %}
</td>
<td>
<a href="{% url 'drug:drug_edit' drug.pk %}" class="btn btn-sm btn-outline-primary">编辑</a>
<a href="{% url 'drug:drug_delete' drug.pk %}" class="btn btn-sm btn-outline-danger"
onclick="return confirm('确定要删除该药品吗?');">删除</a>
</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center">暂无药品数据</td></tr>
{% endfor %}
</tbody>
</table>
{% include 'pagination.html' %}
{% endblock %}
删除操作加一个confirm确认弹窗,是管理系统最基本的安全习惯——药品数据属于基础数据,误删的代价极大。 更严谨的做法是在后端也做一个“假删除”(软删除),即不真正从数据库删除,而是标记is_active=False。如果你的老师没有强制要求物理删除,我建议你用软删除。
4.3 入库单新增页面:前端动态增删行
入库单的特点是“一单多品”,新增入库单时,用户需要在一张单子里选多种药品、填多个数量。这个前端交互用原生的JavaScript实现动态添加行就行,不需要引入Vue等重型框架。
核心逻辑是:页面上有一个药品明细的列表区域,用户点“添加明细”按钮,就动态往列表里插入一行(包含药品选择下拉框、数量输入框、单价输入框、小计和删除按钮)。提交时,JavaScript把所有明细数据收集到一个数组里,通过一个隐藏字段的值或者按约定命名格式提交到后端。
我简化一下关键逻辑:
html复制<div id="detail-container">
<!-- 动态行会被插入到这里 -->
</div>
<button type="button" id="add-detail-btn" class="btn btn-outline-secondary">添加药品明细</button>
配合JavaScript(我用了原生写法,方便你直接看逻辑):
javascript复制let detailIndex = 0;
document.getElementById('add-detail-btn').addEventListener('click', function() {
const container = document.getElementById('detail-container');
const row = document.createElement('div');
row.className = 'row detail-row mb-2';
row.innerHTML = `
<div class="col-md-5">
<select name="drug_id_${detailIndex}" class="form-select drug-select" required>
<option value="">请选择药品</option>
{% for drug in all_drugs %}
<option value="{{ drug.pk }}">{{ drug.drug_name }}({{ drug.specification }})</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<input type="number" name="quantity_${detailIndex}" class="form-control" placeholder="数量" min="1" required>
</div>
<div class="col-md-2">
<input type="number" name="price_${detailIndex}" class="form-control" placeholder="单价" min="0" step="0.01" required>
</div>
<div class="col-md-2">
<input type="text" name="expiry_date_${detailIndex}" class="form-control" placeholder="有效期至" required>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger remove-row-btn">删除</button>
</div>
`;
container.appendChild(row);
detailIndex++;
});
document.getElementById('detail-container').addEventListener('click', function(event) {
if (event.target.classList.contains('remove-row-btn')) {
event.target.closest('.detail-row').remove();
}
});
为什么入库时就要让用户填有效期?因为医药入库是按批次管理的,同一种药,不同批次有效期不同,不能混在一起。这一步把批次概念直接落到了入库操作里,后面的库存查询、效期预警都依赖这个字段。
4.4 数据导出:让Excel表格替你“跑腿”
医药系统的报表数据免不了要导出Excel。我推荐用openpyxl这个库,它不需要Excel环境,直接在Python里生成.xlsx文件。
python复制from openpyxl import Workbook
from django.http import HttpResponse
def export_drugs_excel(request):
wb = Workbook()
ws = wb.active
ws.title = '药品清单'
headers = ['药品编码', '药品名称', '规格', '生产厂家', '当前库存', '采购价', '销售价', '有效期至']
ws.append(headers)
drugs = Drug.objects.all()
for drug in drugs:
ws.append([
drug.drug_code,
drug.drug_name,
drug.specification,
drug.manufacturer,
drug.stock_quantity,
float(drug.purchase_price),
float(drug.sale_price),
drug.expiry_date.strftime('%Y-%m-%d') if drug.expiry_date else '',
])
response = HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = 'attachment; filename=drug_list.xlsx'
wb.save(response)
return response
这样一个视图函数,配合URL路由,就能实现“一键导出药品清单Excel”。用openpyxl导出还有一个好处:它不需要服务器上装Microsoft Office,在Linux服务器上也能正常工作。注意DecimalField字段导出前要转成float或str,否则openpyxl会报类型错误,这也是我踩过的坑。
5. 数据库设计与部署全程:从SQLite到MySQL
开发阶段用Django默认的SQLite数据库,零配置、文件型数据库,打开就能用。但真要部署上线,我建议切到MySQL或PostgreSQL,毕竟SQLite的并发能力和数据量支撑有限,适合开发调试,不适合正式环境长期跑业务数据。
5.1 数据库迁移:双数据库的无缝切换
Django的Migrations机制让数据库切换变得异常简单。开发时用SQLite,部署时切换到MySQL,我告诉你具体怎么做。
第一步:在settings.py里配置MySQL作为生产数据库:
python复制DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'medical_system',
'USER': 'your_db_user',
'PASSWORD': 'your_db_password',
'HOST': '127.0.0.1',
'PORT': '3306',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
第二步:安装MySQL驱动:
bash复制pip install pymysql
在medical_system/__init__.py里加一行:
python复制import pymysql
pymysql.install_as_MySQLdb()
第三步:执行迁移命令,Django会自动根据之前的Model定义,把数据表结构同步到MySQL中:
bash复制python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
注意:
makemigrations在修改Model之后执行,生成迁移文件;migrate把迁移文件应用到数据库。如果改了Model字段加了约束,别忘记先makemigrations再migrate,这是Django项目最常犯的低级错误。
5.2 静态文件与媒体文件配置:别让CSS、图片丢在404里
上线部署时,Django默认不直接提供静态文件服务(CSS、JS、图片)。你需要先收集所有静态文件到一个统一目录,再交给Nginx处理。
python复制# settings.py
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
开发阶段为了省事,可以在urls.py临时加一个静态文件路由:
python复制from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
但注意,这个只适合开发调试,生产环境一定要交给Nginx或者云厂商的对象存储,否则Django跑静态文件效率太低。
5.3 Nginx + Gunicorn + MySQL:服务器部署的完整链路
生产环境部署,我推荐的组合是:Gunicorn(应用服务器) + Nginx(反向代理) + MySQL(数据库)。这套方案在CentOS或Ubuntu服务器上都很成熟。
安装Gunicorn并启动:
bash复制pip install gunicorn
# 启动Django项目,绑定到本地端口
gunicorn medical_system.wsgi:application --bind 127.0.0.1:8000 --workers 3
--workers 3表示启动3个工作进程,可以根据服务器CPU核心数调整,一般设为2 * CPU核心数 + 1。
然后配置Nginx反向代理,把外部请求转发给Gunicorn:
nginx复制server {
listen 80;
server_name your_domain_or_ip;
location /static/ {
alias /path/to/your/project/staticfiles/;
}
location /media/ {
alias /path/to/your/project/media/;
}
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;
}
}
Nginx在这里的作用是:处理静态文件请求(效率远高于Gunicorn)、反向代理转发、缓存、负载均衡。用户访问图片和CSS时,Nginx直接响应,不劳烦Python进程;用户访问页面数据时,Nginx再把请求转给Gunicorn处理。
5.4 运维安全必做项:Settings配置与敏感信息保护
部署上线后,有几个安全配置必须立刻修改:
python复制# settings.py 生产环境必改
DEBUG = False
ALLOWED_HOSTS = ['your_domain_or_ip']
SECRET_KEY = '重新生成一个足够随机的密钥,不要用开发环境的默认值'
DEBUG = False不能省略,否则Django报错时会暴露服务器路径、数据库配置等敏感信息,攻击者可以直接拿去利用。ALLOWED_HOSTS要限定你的域名或IP,防止别人通过伪造Host头把请求打到你的服务器。SECRET_KEY用于密码加密、Session签名,务必通过环境变量或配置文件注入,不要明文写死在代码里:
python复制import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'fallback-insecure-key')
这套部署链路走完之后,你在浏览器里输入服务器的IP或域名,看到的就是一套可以独立运行的医药信息管理系统了。
6. 数据安全与系统加固:医药数据不是闹着玩的
我见过太多管理系统做完就丢一边,“能跑就行”是大多数课设和练手项目的通病。但在这个项目里,你要意识到你处理的数据类型很特殊——药品、库存、供应商,这些数据一旦丢失或篡改,后果不是“重写一下”能解决的。这里分享几个我实际项目中坚持使用的加固策略。
6.1 数据备份:每天都该做的“保险”
用Django开发,数据库备份可以写一个简单的脚本,定期自动执行。以MySQL为例:
bash复制mysqldump -u username -p database_name > backup_$(date +%Y%m%d_%H%M%S).sql
你可以把这条命令放到cron定时任务里,每天凌晨自动备份:
bash复制0 2 * * * /usr/bin/mysqldump -u username -p'password' medical_system > /backup/medical_$(date +\%Y\%m\%d).sql
如果你用的是SQLite,备份更简单——直接把.db文件复制一份就行了。我还见过一个更省心的方案:把SQLite数据库文件放进Git仓库里,每次修改后自动提交,虽然不太优雅,但对单人开发来说确实有效。不过要注意,SQLite文件包含用户密码的哈希,放进公共Git仓库之前要确认仓库是私有的。
6.2 操作日志与审计:出了问题能找到人
我在前面已经强调过OperationLog的重要性。这里补充一个设计细节:日志表里不仅要记“谁、什么时候、做了什么”,还要记录操作前后的数据快照。比如修改药品价格,日志内容除了“修改药品A的价格”,最好还能存一个old_value和new_value字段,或者干脆把修改前后的JSON序列化存进去。
python复制class OperationLog(models.Model):
operator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
operation_type = models.CharField('操作类型', max_length=32)
operation_content = models.TextField('操作内容')
old_value = models.TextField('操作前数据', blank=True, null=True)
new_value = models.TextField('操作后数据', blank=True, null=True)
ip_address = models.GenericIPAddressField('IP地址', null=True, blank=True)
created_at = models.DateTimeField('操作时间', auto_now_add=True)
这个设计看似多存了冗余数据,但在实际发生纠纷或审计时,价值超乎想象。医药行业如果有监管检查,你拿得出完整审计日志,就是系统的硬实力体现。
6.3 效期与库存预警:用邮件和Webhook主动通知
前面提到列表页展示预警状态,但更专业的管理系统应该是“主动通知”,而不是等用户点开页面才发现。Django的django.core.mail可以配置SMTP发邮件,每天定时扫描库存和效期预警,把预警列表发到管理员邮箱。
用Django的manage.py命令机制,我可以写一个自定义命令:
python复制# stock_manage/management/commands/check_warnings.py
from django.core.management.base import BaseCommand
from django.core.mail import send_mail
from drug_manage.models import Drug
from datetime import timedelta
class Command(BaseCommand):
help = '检查库存预警和效期预警并发送邮件'
def handle(self, *args, **options):
low_stock_drugs = Drug.objects.filter(stock_quantity__lte=F('low_stock_threshold'))
expiring_drugs = Drug.objects.filter(
expiry_date__lte=date.today() + timedelta(days=90),
expiry_date__gte=date.today()
)
# 构建邮件内容并发送
...
然后配合cron定时执行:
bash复制0 8 * * * cd /path/to/project && /path/to/venv/bin/python manage.py check_warnings
每天早8点,系统自动检查一遍所有药品的库存和效期,给管理员发邮件报告。这套自动化机制让系统的价值从“记录工具”升级为“管理工具”。
7. 测试与排错:常见的坑与我的排查经验
写代码时一帆风顺是不存在的。我把自己在开发医药系统过程中踩过的几个典型坑列出来,希望能帮你节省排查时间。
7.1 保存入库单时“药品库存没变”的坑
现象描述:入库单保存成功了,但药品的库存数字纹丝不动。
排查过程:我首先检查库存更新代码是否真的在视图里执行。结果发现,我在StockInRecord.objects.create(...)时用了@transaction.atomic()包装,但更新库存的代码写在函数末尾,因为某个条件校验没有通过,提前return了,整个事务回滚,导致入库单和库存更新“同生共死”。后来我把库存更新逻辑放到事务的核心区域,并且在浏览器里用F12刷新Network面板查看响应,确认入库单是真的“保存成功”还是“事务回滚”了。
最终原因:transaction.atomic是好的,问题在于当库存更新抛异常时,异常信息被视图层捕获并吞掉了,前端看到“入库成功”,但实际数据库已经回滚。这个坑很隐蔽,解决方法是:在视图里把异常信息打到日志文件,或者在调试阶段直接让异常抛出来显示在页面上。
经验:不要相信“前端说成功就是成功”,一定要看数据库里的真实数据。DEBUG阶段把
DEBUG = True开着,异常信息一目了然。
7.2 “日期格式化”报错
现象描述:页面展示有效期时,出现'NoneType' object has no attribute 'strftime'。
排查过程:因为药品数据的expiry_date字段可能是空的,在模板里直接调用了drug.expiry_date|date:"Y-m-d"。Django的date过滤器本身能处理None,但我在导出Excel的代码里直接调用了drug.expiry_date.strftime('%Y-%m-%d'),就没做空值判断。
解决方案:
python复制if drug.expiry_date:
expiry_str = drug.expiry_date.strftime('%Y-%m-%d')
else:
expiry_str = ''
一句话:任何从Model取出来的可空字段,在处理前都要先判空。 这个习惯能帮你避开80%的运行时异常。
7.3 “部署后登录页面样式全没了”的坑
现象描述:本地运行时页面样式正常,部署到服务器后,登录页、列表页全部变成“毛坯房”——HTML元素都在,但完全没样式。
排查过程:Nginx静态文件配置路径写错了。STATIC_ROOT收集到的静态文件目录跟Nginx的alias路径不一致,导致浏览器请求CSS文件返回404。
解决方案:部署前先在项目目录执行:
bash复制python manage.py collectstatic
确认staticfiles目录下真的有文件。然后核对Nginx配置文件里的路径,跟STATIC_ROOT保持一致。改完配置记得重启Nginx:
bash复制sudo nginx -t
sudo systemctl reload nginx
7.4 “数据库中文乱码”的坑
现象描述:药品名称、供应商名称等中文字段全是???。
排查过程:MySQL数据库或表的字符集没有设置成utf8mb4。Django的settings.py里数据库配置加上:
python复制OPTIONS = {
'charset': 'utf8mb4',
}
同时在创建MySQL数据库时指定字符集:
sql复制CREATE DATABASE medical_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
utf8mb4能完整支持中文和emoji,不要用老旧的utf8,它存不了4字节的Unicode字符。
8. 这个项目还能怎么延伸:从课设到真正能用的产品
一套医药信息管理系统做出来,如果只停在“技术演示”层面,那它的价值只发挥了一小半。我建议你在基础功能完成后,往这几个方向延伸,无论对答辩还是对实际应用都价值巨大。
对接条码扫描。药品入库出库时,用扫码枪扫药品条码,自动带入药品信息,大幅提升操作效率。Django后端只需要增加一个“按条码查询药品”的接口,前端可以在入库表单里加一个扫码输入框,扫码枪本质上是键盘,输入一串条码后自动触发查询,无需额外硬件开发。
增加数据分析报表。比如:本月入库总金额趋势图、供应商供货量排行、库存周转率分析。Django后端返回JSON数据,前端可以用ECharts或者Chart.js画图。这个延伸方向尤其适合毕业设计,展示效果非常直观,而且代码量不大。
引入审核流程。药品入库单不是提交就生效,而是先由操作员填写,再由主管审核。审核后才正式更新库存。这个功能用Django的status字段加几个视图就能实现,但对业务的严谨性提升不是一点半点。
增加到期药品自动锁定。药品有效期过了之后,系统自动禁止该药品出库销售。实现方式也很简单,在出库视图里加一个过滤条件:expiry_date__gt=timezone.now().date()。
每个延伸方向都是一道很好的加分题。我的建议是:先把基础功能做到稳定可靠,再按自己的兴趣挑一个方向深入。 一个做得完整的特色功能,比五个半成品功能更有说服力。
从我自己的开发经历来看,医药信息管理系统是个非常锻炼人的Django实战项目。它既有典型的CRUD基础功能,又有事务、权限、预警、报表这些贴近真实业务的需求,做完一套下来,你对Django的理解会上一个台阶。技术上的问题都好解决,真正难的是把业务逻辑想通透——数据怎么流转,状态怎么切换,异常怎么处理,这些才是架构师的思考方式。希望这篇拆解能给你一些启发,让你在动手写代码之前,心里先有一张清晰的地图。
