Django+MySQL药品管理系统实战:从数据库设计到部署上线
简介本资源是一个基于Django框架与MySQL数据库构建的药品管理系统实战项目面向Python Web开发初学者及医药信息化相关从业者解决药品库存、供应商、销售记录等核心业务的数字化管理需求。压缩包共118个文件含39个Python源码涵盖models、views、urls等Django核心模块、43个HTML模板如药品列表、入库出库、供应商管理等页面、28个pyc编译文件及少量静态资源整体体积仅148KB结构清晰便于快速部署与二次开发。已有2087人学习下载项目完整实现了药品增删改查、模糊检索、库存实时更新、权限分级控制及表单验证等典型功能代码注释充分模板采用Django内置模板语言配合响应式基础布局可直接运行并作为课程设计、毕设参考或企业轻量级药管系统原型。 做药品管理系统之前我建议你先想清楚一件事这类系统的核心不在“增删改查”而在“数据一致性和可追溯性”。我最早接手过一个药房管理项目最初的版本只做了药品入库和出库登记结果运营三个月后库存对不上、批号追溯查不到源头、近效期药品没人提醒最后全部推倒重来。所以这篇博文我不打算只教你跑一个 Django 项目而是把“djangomysql 药品管理系统”从需求拆解到数据库设计、从核心代码到部署上线完整地过一遍把我踩过的坑和后来总结出来的方案都写出来。如果你是刚学 Django 的学生或者正在为公司做内部药品管理系统的开发人员这篇文章能帮你少走很多弯路。1. 系统整体设计与技术选型的思考1.1 先搞清楚药品管理系统到底管什么药品管理系统不等于“药品列表增删改查”。一个能真正用于药房、药店或医院科室的药品管理系统至少要覆盖药品基础信息、供应商管理、采购入库、销售出库、库存盘点、近效期预警、批号追溯、用户权限这几大块。我自己在做需求调研时习惯先画一条“药品生命周期线”药品从供应商进来经过验收、入库、上架到被销售或处方使用中间还可能发生退货、报损、移库。系统里每一个环节都要留痕这样才能回答“这个批号的药是谁进的、什么时候用完的、还剩多少”这类问题。所以第一个建议是动手写代码之前先列出核心业务对象和它们之间的关系不要一上来就建表。药品管理系统最常见的对象有这些药品信息药品名称、通用名、规格、生产厂商、批准文号、剂型、单位、零售价、进货价药品库存关联药品、批号、生产日期、有效期、库存数量、货位供应商名称、联系人、电话、资质证号、地址入库单/入库明细入库单号、供应商、入库时间、经手人、明细行出库单/出库明细出库单号、客户或科室、出库时间、经手人、明细行库存流水每次入库、出库、盘点调整都生成一条流水记录预警记录近效期或低库存触发的提醒用户与角色管理员、采购员、库管员、收银员这些对象的关系理顺了技术实现就是水到渠成的事。1.2 为什么选 Django MySQL而不是其他组合技术选型上Django MySQL 是这类管理系统非常稳的组合。Django 自带强大的 ORM对象关系映射、Admin 后台、认证系统、表单处理而且有完整的迁移机制开发周期比 Spring Boot 那一套短得多。MySQL 则是久经考验的开源关系型数据库部署简单、运维资料多、社区成熟对于药品管理系统这种以事务处理为主、对数据一致性有较高要求的场景非常合适。有人会问那为什么不用 PostgreSQL其实 PostgreSQL 也很好但选择 MySQL 主要有三个实际原因服务器环境大多是 CentOS、Rocky Linux 或 Ubuntu这些系统上安装 MySQL 非常方便资料多出了问题容易搜到答案。团队或学校课程里普遍熟悉 MySQL后续维护成本低。如果以后要部署到云服务器云厂商的 RDS 默认就是 MySQL兼容性最好。Django 这边无论你用的是 Python 3.8 还是 3.11Django 4.x LTS 版本对我来说最合适因为它在 ORM、自动化和安全机制上都比较完善。如果你用的是 MySQL 5.7建议搭配 django 2.2 到 3.2 这个区间如果是 MySQL 8.0直接上 Django 4.x 也没有任何问题这一点在后面的配置部分我会再展开讲。2. 数据库设计与 Django 模型实现2.1 药品信息表和库存表怎么设计才不踩坑数据库设计是整个系统最核心的部分比写视图函数重要得多。我先给出药品信息表的设计建议这是所有功能的基石。药品信息表drug_info应该包含class DrugInfo(models.Model): drug_code models.CharField(max_length50, uniqueTrue, verbose_name药品编码) generic_name models.CharField(max_length128, verbose_name通用名) trade_name models.CharField(max_length128, blankTrue, nullTrue, verbose_name商品名) specification models.CharField(max_length64, verbose_name规格) dosage_form models.CharField(max_length32, verbose_name剂型) manufacturer models.CharField(max_length128, verbose_name生产厂商) approval_number models.CharField(max_length64, verbose_name批准文号) unit models.CharField(max_length16, verbose_name单位) reference_price models.DecimalField(max_digits10, decimal_places2, verbose_name零售价) purchase_price models.DecimalField(max_digits10, decimal_places2, verbose_name进货价) is_active models.BooleanField(defaultTrue, verbose_name是否启用) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) class Meta: db_table drug_info verbose_name 药品信息 verbose_name_plural 药品信息 indexes [ models.Index(fields[generic_name]), models.Index(fields[drug_code]), ]这里有几个非常关键的细节drug_code 必须唯一。这个编码是自己生成的还是国家药品编码都行但一旦确定入库出库全部通过它来关联千万不能允许重复。价格用 DecimalField不要用 FloatField。药品价格涉及到金额计算浮点数会出现 0.1 0.2 不等于 0.3 这种问题而 DecimalField 在 Python 层面通过 decimal 类型保证精确。通用名和商品名分开存。很多人把药品名称只设计成一个字段后期在检索和统计的时候会非常痛苦因为同一通用名的药品可能有多个厂商、多个商品名。库存表drug_stock的设计稍微复杂一些因为需要支持同一药品不同批号分开管理class DrugStock(models.Model): drug models.ForeignKey(DrugInfo, on_deletemodels.PROTECT, verbose_name药品) batch_no models.CharField(max_length64, verbose_name批号) production_date models.DateField(verbose_name生产日期) expiry_date models.DateField(verbose_name有效期至) quantity models.IntegerField(default0, verbose_name库存数量) location models.CharField(max_length64, blankTrue, nullTrue, verbose_name货位) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) class Meta: db_table drug_stock verbose_name 药品库存 verbose_name_plural 药品库存 constraints [ models.UniqueConstraint(fields[drug, batch_no], nameuniq_drug_batch) ]注意我在外键上用了on_deletemodels.PROTECT意味着有库存记录的药品不允许直接删除。药品不能删只能停用这是药品管理系统的硬性规则。因为药品一旦有入库出库记录删除药品会把历史记录全部搞乱批号追溯就断了。这里的教训是凡是涉及财务和溯源的业务一律逻辑删除不要物理删除。2.2 Django 迁移流程与 MySQL 配置细节模型写好后接下来就是迁移。迁移之前先确定 Django 能连上 MySQL。第一步安装数据库驱动。Django 连接 MySQL 需要 mysqlclient但在 Windows 上安装 mysqlclient 经常失败。我推荐两种方案Linux 环境下先安装依赖再安装sudo apt install python3-dev default-libmysqlclient-dev build-essential然后pip install mysqlclient。如果你用的是 Windows直接装pymysql然后在项目__init__.py里加两行import pymysql pymysql.install_as_MySQLdb()第二步修改 settings.pyDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: drug_db, USER: drug_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }注意charset一定要设置成utf8mb4。药品名称里可能出现生僻字或者特殊符号utf8mb4 才能完整支持之前用默认字符集导致个别生僻药品名存进去变成了问号排查了很久。创建完数据库后执行python manage.py makemigrations python manage.py migrate迁移成功后可以用python manage.py createsuperuser创建管理员账号。Django 自带的 Admin 后台在这一步就能用了输入/admin就能通过可视化界面管理用户和权限。很多人不知道的是Django Admin 不只是给开发者用的它可以作为药品管理系统的“初级后台”先让业务方录入基础数据等前端页面做好后再切换非常方便。2.3 Django Admin 的定制技巧Admin 后台默认展示的是药品对象的__str__返回值想要体验好需要做一些定制。在 admin.py 里这样写admin.register(DrugInfo) class DrugInfoAdmin(admin.ModelAdmin): list_display (drug_code, generic_name, trade_name, specification, manufacturer, reference_price, is_active) list_filter (is_active, dosage_form, manufacturer) search_fields (drug_code, generic_name, trade_name, manufacturer) list_per_page 20 ordering (drug_code,)这样配置后后台就能按药品编码、名称搜索还能按剂型、生产厂商筛选。Admin 的search_fields实际会生成 LIKE 查询在数据量不大的情况下没有问题但数据量达到几十万条时建议换成第三方库 Django Admin 的autocomplete_fields或者直接用下面的搜索接口方案。3. 核心业务功能的代码实现3.1 药品入库与出库的事务处理药品入库和出库操作必须在一个数据库事务里完成因为这个过程会同时修改“库存表”和“流水表”任何一步失败都会导致数据不一致。入库操作的逻辑是拿到入库单明细对每一条药品检查该药品该批号的库存记录是否存在如果存在就把数量累加如果不存在就新建一条库存记录。同时向库存流水表stock_transaction插入一条“入库”记录。from django.db import transaction from django.db.models import F from django.utils import timezone def stock_in(request, drug_id, batch_no, production_date, expiry_date, quantity, operator): with transaction.atomic(): drug DrugInfo.objects.select_for_update().get(iddrug_id) stock, created DrugStock.objects.select_for_update().get_or_create( drugdrug, batch_nobatch_no, defaults{ production_date: production_date, expiry_date: expiry_date, quantity: 0, } ) stock.quantity F(quantity) quantity stock.save(update_fields[quantity, updated_at]) StockTransaction.objects.create( drugdrug, batch_nobatch_no, trans_typein, quantityquantity, operatoroperator, remarkf入库单{request_no} )这里有两个要点select_for_update()会锁定对应行防止并发情况下两个人同时入库导致数量覆盖。这是我在高并发测试中总结出来的不加锁的话库存数据在并发场景下会出大问题。使用F(quantity) quantity而不是直接读出数量再加上去是为了把加减操作放到数据库层面执行避免读改写三步中间插入其他事务。出库操作类似但要加一步判断if stock.quantity quantity: raise ValueError(库存不足无法出库)这个检查必须在锁内完成。如果不加锁两个窗口同时出库同一批次的药很容易出现库存扣成负数的情况。我见过不止一次这种事故所以这里一定要写清楚。3.2 药品检索和多条件筛选的 MySQL 查询优化药品管理系统中检索是使用频率最高的功能。一个实用的检索接口应该支持按药品编码精确匹配、按通用名模糊匹配、按生产厂商筛选、按有效期范围过滤还要分页。如果用 Django ORM 写可以这样组织from django.db.models import Q def search_drugs(request): keyword request.GET.get(keyword, ) manufacturer request.GET.get(manufacturer, ) expiry_start request.GET.get(expiry_start, ) expiry_end request.GET.get(expiry_end, ) queryset DrugStock.objects.select_related(drug).all() if keyword: queryset queryset.filter( Q(drug__drug_code__icontainskeyword) | Q(drug__generic_name__icontainskeyword) | Q(drug__trade_name__icontainskeyword) ) if manufacturer: queryset queryset.filter(drug__manufacturer__icontainsmanufacturer) if expiry_start: queryset queryset.filter(expiry_date__gteexpiry_start) if expiry_end: queryset queryset.filter(expiry_date__lteexpiry_end) queryset queryset.order_by(expiry_date) paginator Paginator(queryset, 20) page paginator.get_page(request.GET.get(page)) return page当数据量变大时icontains会转换成LIKE %keyword%这种写法无法用普通索引加速。如果系统里药品数据达到十万条以上建议做三个优化给drug_code建唯一索引精确查询走索引速度几乎无感知。模糊搜索建议增加全文索引MySQL 的 FULLTEXT或者引入 Elasticsearch不过对于大多数中小型药品管理系统来说还到不了这一步。给expiry_date建普通索引因为近效期筛选expiry_date__ltexxx能用到索引。还有一个非常实用的 MySQL 小技巧分页优化。当页数很大的时候LIMIT 100000, 20会越来越慢因为 MySQL 要扫描前面十万行。改成“先取起始位置的主键再关联查询”可以显著提升性能SELECT * FROM drug_stock WHERE id (SELECT id FROM drug_stock ORDER BY id LIMIT 100000, 1) ORDER BY id LIMIT 20;在 Django ORM 里这个需求可以直接用Paginator解决但业务方反馈“翻到第几千页很慢”的时候再用上面这条 SQL 优化。3.3 近效期预警与低库存提醒药品管理的灵魂功能之一就是近效期预警。药品一旦过期不但不能出售处理起来还涉及报损、销毁记录。预警逻辑很简单从当前日期开始算到有效期剩余天数小于设定阈值就提醒。我通常会在模型层加一个属性用来判断是否近效期def is_near_expiry(self, threshold_days90): from datetime import timedelta return self.expiry_date timezone.now().date() timedelta(daysthreshold_days)然后通过 Django 的管理命令management command定期扫描生成预警记录class Command(BaseCommand): def handle(self, *args, **options): threshold_days 90 near_expiry_stocks DrugStock.objects.filter( expiry_date__ltetimezone.now().date() timedelta(daysthreshold_days), quantity__gt0 ).select_related(drug) for stock in near_expiry_stocks: DrugWarning.objects.update_or_create( drugstock.drug, batch_nostock.batch_no, defaults{warning_type: near_expiry, warning_date: timezone.now().date()} )定时任务建议用系统 crontab 每天执行一次0 9 * * * cd /path/to/project /usr/bin/python3 manage.py check_warning低库存预警逻辑类似把库存数量与设定的最低库存阈值比较即可。这里的核心思路是预警要落到独立的表里而不是每次打开页面现算。因为现算每次都要全表扫描定时任务在后台跑算了存起来前端展示效率高得多。3.4 用户权限设计和操作日志记录药品管理系统通常涉及多个角色管理员、采购员、库管员、收银员。不同角色能看到的功能和能执行的操作应当严格区分。Django 自带的认证系统提供了 Group 和 Permission可以直接复用。在初始化时创建三个分组管理员组拥有所有权限采购员组能够添加和修改药品信息能够做入库操作但不可以出库库管员组能够查看库存、做盘点调整、处理近效期药品实现方式是在 view 上加装饰器from django.contrib.auth.decorators import login_required, permission_required login_required permission_required(drug.add_druginfo, raise_exceptionTrue) def drug_create(request): ...权限控制之外操作日志是药品管理系统必不可少的一环。每一次入库、出库、盘点、价格修改都必须记录操作人、操作时间、操作前后的值。Django 的django-auditlog第三方库可以很方便地记录模型变更但更轻量、更可控的方法是自定义日志表class OperationLog(models.Model): user models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, verbose_name操作人) action models.CharField(max_length64, verbose_name操作类型) target_type models.CharField(max_length64, verbose_name对象类型) target_id models.CharField(max_length64, verbose_name对象ID) detail models.TextField(blankTrue, nullTrue, verbose_name操作详情) created_at models.DateTimeField(auto_now_addTrue, verbose_name操作时间)在关键视图函数里手动写一行OperationLog.objects.create(...)就行。虽然比中间件多一点代码但胜在可控性强想查什么都有清晰的结构。4. 部署上线与高发问题排查4.1 从开发环境到生产环境的关键改动开发环境里 Django 默认的python manage.py runserver只适合本地调试生产环境必须要搭 WSGI 服务器和 Web 服务中间层。常见的部署方案是 Nginx Gunicorn Django MySQL。先把 Django 项目的 settings.py 改好DEBUG False ALLOWED_HOSTS [your-domain.com, your-server-ip] STATIC_ROOT /var/www/drug_system/static # 数据库连接池建议开启 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: drug_db, USER: drug_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }ALLOWED_HOSTS必须在生产环境显式配置不然 Django 会拒绝所有非本地请求直接返回 400。STATIC_ROOT配置好后执行python manage.py collectstatic把所有静态文件收集到指定目录交给 Nginx 处理。Gunicorn 启动命令写成这样比较稳妥gunicorn drug_system.wsgi:application --bind 127.0.0.1:8000 --workers 3workers 数量一般设为 CPU 核数的 2 到 4 倍我常用的公式是workers CPU核心数 * 2 1。如果服务器是 2 核就开 5 个 worker再高反而因为上下文切换导致性能下降。Nginx 的配置片段供参考server { listen 80; server_name your-domain.com; location /static/ { alias /var/www/drug_system/static/; } 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; } }4.2 MySQL 连接数、时区、字符集这些隐藏坑部署过程中最容易踩的是 MySQL 侧的配置问题。第一个坑是数据库连接数不够。Django 每个请求都会占用一个数据库连接Gunicorn 起了 5 个 worker每个 worker 可能有多个线程加在一起连接数轻松超过 151MySQL 默认最大连接数。解决办法是修改 MySQL 配置[mysqld] max_connections 500然后在 Django 里也加连接复用参数OPTIONS: { charset: utf8mb4, connect_timeout: 5, }第二个坑是时区问题。Django settings.py 里的TIME_ZONE建议设置为Asia/Shanghai同时把USE_TZ设为True。这里有一件很微妙的事当USE_TZTrue时Django 写入 MySQL 的数据是按 UTC 时间存储的取出来再转本地时间。如果业务方直接查看数据库可能会觉得时间少了 8 小时。解决方法有两条路保持USE_TZTrue所有时间展示交给 Django 模板自动转换。或者干脆USE_TZFalse让 Django 直接存本地时间但这样会失去时区支持不推荐在正规项目中使用。第三个坑是 MySQL 8.0 的认证方式问题。MySQL 8.0 默认使用caching_sha2_password认证而较旧版本的 mysqlclient 可能不支持导致连接时报Authentication plugin caching_sha2_password cannot be loaded。解决办法是创建用户时指定mysql_native_passwordCREATE USER drug_user% IDENTIFIED WITH mysql_native_password BY your_password; GRANT ALL PRIVILEGES ON drug_db.* TO drug_user%; FLUSH PRIVILEGES;4.3 常见问题速查表根据我接触过的和网上大量同类项目的反馈这里整理一份高频问题排查表基本覆盖了“djangomysql 药品管理系统”开发中 70% 的报错场景问题现象原因解决办法启动时ModuleNotFoundError: No module named MySQLdb没有安装 mysqlclient 或 pymysql按上文方法安装驱动或使用 pymysql 注册访问页面报 500日志提示table doesnt exist忘记执行迁移python manage.py migrateconnect timeout连接数据库失败MySQL 服务未启动或 host/port 配置错误systemctl status mysql检查服务状态确认连接参数中文数据存入数据库变成?数据库/表字符集不是 utf8mb4修改库表字符集ALTER TABLE drug_info CONVERT TO CHARACTER SET utf8mb4接口报DataError: Out of range value字段长度不够或者数据超出范围检查模型字段 max_length/max_digits调整迁移库存数量并发扣成负数没有使用数据库锁或事务给库存操作加上select_for_updatetransaction.atomic创建外键时报 1215字段类型或字符集不一致确保关联字段类型和字符集一致比如都是int或都是utf8mb4权限判断总是 403raise_exceptionTrue但没有登录登录后访问或取消异常抛出改为重定向登录页从我的经验来看超过一半的部署问题不是 Django 代码的问题而是 MySQL 服务和配置的问题。遇到报错先看 MySQL 错误日志一般能直接定位。4.4 一个反向教训不要一上来就写前端页面最后想分享一个很多人会犯的方向性错误一开始就把精力放在写漂亮的 HTML 页面和复杂的 JavaScript 交互上结果后端的业务逻辑一塌糊涂。我现在的开发习惯是先用 Django Admin 把基础数据管理跑通让用户真实录入几个星期的数据看业务流程有没有问题确认之后再逐步用 Bootstrap、Vue 等替换成正式界面。这样做的原因是管理系统的核心价值在于数据和逻辑的正确性页面的美观程度永远是次要的。把库存、流水、预警这些骨骼搭结实了外面穿什么衣服都很容易。这也让我养成了一个习惯在开发任何管理系统之前先做一个小范围的“可用性确认”。拿一两天时间把最小核心闭环跑通也就是药品信息维护、入库、出库、库存查询这四个功能串成一个完整的流程然后找实际使用者试用。这个闭环如果不顺后面的功能做得再多也是白搭。药品管理系统的痛点从来不在技术难度而在于需求是不是真的理清楚了。很多项目翻车都是因为开发者在“做功能”而不是“解决问题”。你把核心业务先跑通了后面加角色权限、加报表、加消息提醒都是水到渠成的事。5. 扩展想法这个系统还能怎么延伸药品管理系统做完基础版本之后可以往几个方向扩展这也是实际业务中很有价值的模块。第一是处方关联。如果这个系统用在诊所或医院科室出库记录应当关联到处方单上。设计处方主表和处方明细表然后把出库明细与处方明细关联起来这样既能追溯“这批药是谁开的处方”也能统计科室用药情况。第二是报表统计。基于库存流水表可以按天、按周、按月统计药品入库量、出库量、销售额和毛利。Django ORM 里用annotate加TruncMonth就能方便地生成月度统计from django.db.models.functions import TruncMonth from django.db.models import Sum monthly_sales StockTransaction.objects.filter( trans_typeout ).annotate( monthTruncMonth(created_at) ).values(month).annotate( total_quantitySum(quantity), total_amountSum(amount) ).order_by(month)第三是引入扫码枪或二维码。药品入库前打印二维码标签出库时扫码枪扫一下系统自动找到对应批号的库存记录并扣减效率和准确率都远高于手工选择。Django 视图里接收扫码枪输入本质上就是接收一个字符串参数把药品编码带进来就行实现成本不高却极其提升使用体验。第四是数据库的定期备份。MySQL 定时任务生产环境必须做mysqldump -u drug_user -p drug_db --single-transaction --routines /backup/drug_db_$(date %Y%m%d).sql--single-transaction参数在 InnoDB 引擎下可以在不锁表的情况下完成备份很关键。恢复时使用mysql -u drug_user -p drug_db backup.sql即可。我个人的经验是只要基础核心模块的数据模型设计得足够稳这些扩展功能都只是“往上添砖”的事。真正难的是刚开始那一步把数据的流向、约束、权限想明白。如果你正在做类似的药品管理系统照着这篇文章的思路去梳理一遍你的表结构和业务闭环会比直接搜索“django 药品管理系统源码”然后复制粘贴有价值得多。源码只能给你一个参考而理清业务逻辑之后写出来的系统才是真正能上线长期使用的系统。本文还有配套的精品资源点击获取