最近接了个课程设计级别的项目要把一个医药信息管理系统完整做出来技术栈锁定 Python Django还要附带数据库脚本和说明文档。这类系统在高校课程设计和毕业设计里出现频率极高很多同学卡在同一个地方框架会用但不知道怎么把业务逻辑和数据库设计得严谨功能能跑但经不起细节追问。这篇文章我把整个项目从设计到落地的完整思路掰开揉碎讲一遍包括数据库怎么建模、Django 里怎么把增删改查写得干净、权限和日志怎么处理、以及最后怎么把系统跑起来。无论你是刚学完 Django 基础、准备做课程设计的学生还是在职想快速上手一个完整 Web 项目开发的开发者这篇文章都能给你一套可以直接抄作业的方案。我会尽量模拟一个真实的开发过程把踩过的坑、改进过的设计、以及源码里值得琢磨的细节都说清楚。1. 系统整体设计与技术选型思路1.1 为什么选 Python Django 这套组合市面上能做 Web 信息管理系统的方案其实不少光是 Python 生态里就有 Flask、FastAPI更别说 Java 的 Spring Boot、PHP 的 Laravel。但这个项目最后敲定 Django我是从三个维度衡量的。第一是开发效率。Django 自带 Admin 后台、ORM、表单处理、认证系统、安全防护这些对一个信息管理系统来说是刚需。医药信息管理系统虽然有自定义的业务逻辑但本质上还是围绕药品、供应商、库存、销售单据的增删改查Django 的脚手架能在一小时内把项目骨架搭完剩下的精力可以全部放在业务细节上。第二是安全性。医药行业的数据敏感度高Django 默认提供的 CSRF 防护、XSS 过滤、SQL 注入防护、密码哈希机制直接省掉了自己造轮子的风险。对于一个课程设计级别的项目来说能开箱即用地满足这些安全需求是很加分的。第三是生态和资料。Django 的官方文档质量极高遇到问题随便一搜就能找到解决方案。对于做课程设计的同学来说这意味着你卡住的时候不会孤立无援。1.2 系统功能模块怎么拆拿到需求后不要急着写代码先把功能模块理清楚。我当时把系统拆成了五个核心模块分别对应不同角色和业务场景。用户认证与权限管理模块是系统的地基负责用户的注册、登录、登出以及操作权限控制。在医药系统里管理员和普通员工能做的事情不一样管理员可以管理用户、查看全部数据普通员工只能操作日常业务流程。药品信息管理模块是整个业务的核心负责药品基本信息的增删改查包括药品名称、批准文号、生产厂家、规格、剂型、有效期等字段。这部分我会在下面的数据库设计里详细展开。库存管理模块处理药品的入库、出库和库存查询关键是库存数量的动态更新和低库存预警。比如某款药品库存低于设定的安全阈值系统要能自动提醒。供应商管理模块记录供应商的基本信息和历史合作记录方便后续对供应商进行评估。销售与出库管理模块负责创建销售单据联动扣减库存同时记录每一笔操作的操作者方便日后审计。模块拆分这件事看上去简单但决定了后面代码的复杂度。我见过不少人把药品和库存塞在一个模型里结果每次出库入库都要小心翼翼地处理字段同步逻辑极其容易出错。正确的做法是把药品基础数据和库存数据分开。1.3 Django 项目目录规划项目目录不是随便建的合理的目录结构能让代码维护轻松一个量级。我采用的目录结构如下medicine_project/ ├── manage.py ├── medicine_system/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── apps/ │ ├── users/ │ ├── drugs/ │ ├── inventory/ │ ├── suppliers/ │ └── sales/ ├── static/ ├── media/ ├── templates/ │ ├── base.html │ ├── users/ │ ├── drugs/ │ ├── inventory/ │ ├── suppliers/ │ └── sales/ ├── db.sqlite3 └── requirements.txt我把不同业务模块拆成了独立的 app应用而不是把所有模型塞在一个 app 里。这样做的好处是各个模块之间的边界清晰一个 app 只负责一类业务。虽然 Django 官方对 app 的划分没有强制要求但项目一大模块拆分的优势就体现出来了。ivorie 规划好之后下一步就是搭数据库。医药信息管理系统的难点不在 Django 代码而在数据模型的设计。2. 数据库设计与 ORM 模型实现2.1 核心数据表的字段规划数据库设计是整个项目最重要的一环字段设计得不好后面写业务代码会处处受制。我当时反复推敲了几张核心表的设计这里把字段和设计理由一并写出来。药品信息表是系统的核心表我设计的字段包括药品名称、通用名、批准文号、生产厂家、规格、剂型、单位、零售价、进货价、有效期、存储条件。批准文号是医药行业里药品的唯一标识类似于人的身份证号在数据库里应该设置为唯一约束防止重复录入。库存表不单独存一个“当前库存”字段而是记录每次入库和出库的流水。这样设计的好处是你随时可以追溯“某个批次进了多少、出了多少、现在还剩多少”而不是只知道一个最终数值。如果真的需要实时库存可以通过聚合函数算出来或者定时把计算结果同步到一张汇总表。供应商表记录供应商编号、名称、联系人、联系电话、地址、经营许可证号。其中经营许可证号应该做唯一约束因为一个合规的供应商只有一个合法证号。销售单据表记录每一次销售行为分为单据主表和明细表。主表存单据编号、销售员、销售时间、总金额明细表存每种药品的数量、单价、小计。说实话在设计数据库时我是照着电商系统的思路来的因为医药销售和电商销售在业务形态上非常相似有商品、有库存、有订单、有订单明细。电商系统经过这么多年的发展数据架构已经很成熟照着它的思路做基本不会出错。2.2 Django 模型代码示例数据表设计好了之后直接用 Django ORM 定义模型。这里给出部分核心模型代码完整的代码在配套源码里。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): 自定义用户模型 phone models.CharField(max_length11, verbose_name手机号) role models.CharField(max_length10, choices( (admin, 管理员), (staff, 员工), ), defaultstaff, verbose_name角色) class Meta: db_table sys_user verbose_name 用户 verbose_name_plural verbose_name def __str__(self): return self.usernamefrom django.db import models class Drug(models.Model): 药品信息表 drug_code models.CharField(max_length20, uniqueTrue, verbose_name药品编码) name models.CharField(max_length100, verbose_name药品名称) generic_name models.CharField(max_length100, verbose_name通用名) approval_number models.CharField(max_length50, uniqueTrue, verbose_name批准文号) manufacturer models.CharField(max_length100, verbose_name生产厂家) specification models.CharField(max_length50, verbose_name规格) dosage_form models.CharField(max_length20, verbose_name剂型) unit models.CharField(max_length10, verbose_name单位) purchase_price models.DecimalField(max_digits10, decimal_places2, verbose_name进货价) retail_price models.DecimalField(max_digits10, decimal_places2, verbose_name零售价) expiry_date models.DateField(verbose_name有效期) storage_condition models.CharField(max_length50, 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 verbose_name def __str__(self): return self.nameclass InventoryRecord(models.Model): 库存流水记录表 RECORD_TYPE ( (in, 入库), (out, 出库), ) drug models.ForeignKey(Drug, on_deletemodels.CASCADE, verbose_name药品) record_type models.CharField(max_length10, choicesRECORD_TYPE, verbose_name记录类型) quantity models.IntegerField(verbose_name数量) operator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, verbose_name操作人) created_at models.DateTimeField(auto_now_addTrue, verbose_name操作时间) class Meta: db_table inventory_record verbose_name 库存流水 verbose_name_plural verbose_name def __str__(self): return f{self.drug.name} - {self.get_record_type_display()}注意几个细节药品批准文号设置了 uniqueTrue防止同一款药品重复录入外键字段用 on_deletemodels.SET_NULL 而不是 CASCADE因为销售记录是审计数据不能因为用户被删除就跟着删掉底账价格字段用 DecimalField 而不用 FloatField因为浮点数在涉及金额时会有精度问题。2.3 数据库迁移实操记录模型定义好之后需要生成并执行迁移脚本。顺序不能乱命令一共三条python manage.py makemigrations python manage.py migrate第一条命令是根据模型的变化生成迁移文件第二条是把迁移文件真正应用到数据库。有一点要特别注意如果你启用了 Django Admin第一次 migrate 时会自动创建用户、权限、日志等内置表所以要在 migrate 完成之后再创建超级用户python manage.py createsuperuser我实际操作中遇到过一个比较坑的情况模型字段改了但迁移命令一直报错。排查半天发现是之前有一次 makemigrations 生成的迁移文件有残留冲突解决办法是把出问题的 app 迁移文件删掉重来。但如果数据库里已经有真实数据就不要随便删迁移文件否则会搞得很难看。正确做法是在模型修改之前规划好字段减少后续的反复修改。这个项目我改了三版才定稿每次改完都要同步改前端模板里的表单组件属于正常但很耗时的迭代过程。3. 核心业务功能与页面实现3.1 药品管理的增删改查完整实现药品管理是所有功能里最核心的增删改查的逻辑要实现得干净且完整。Django 提供了基于类的视图CBV和基于函数的视图FBV两种方式我推荐新手先用 FBV 把逻辑想清楚再看 CBV 的封装。这里用 FBV 展示一个药品列表加搜索的完整实现from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Q from .models import Drug from .forms import DrugForm login_required def drug_list(request): queryset Drug.objects.all() keyword request.GET.get(keyword, ) if keyword: queryset queryset.filter( Q(name__icontainskeyword) | Q(approval_number__icontainskeyword) | Q(manufacturer__icontainskeyword) ) paginator Paginator(queryset, 10) page_number request.GET.get(page) page_obj paginator.get_page(page_number) context { page_obj: page_obj, keyword: keyword, } return render(request, drugs/drug_list.html, context)这里有几个细节值得展开讲。登录保护使用的是 login_required 装饰器用户未登录时会被重定向到登录页这是 Django 内置的认证机制。搜索功能用 Django ORM 的 Q 对象把多个搜索条件用或or关系叠加实现按名称、批准文号、厂家三个维度的模糊搜索。分页用 Paginator 类每页显示 10 条数据模板层通过 page_obj 对象渲染分页导航。药品的新增和编辑逻辑因为表单提交方式相同我合并成了同一个视图login_required def drug_edit(request, pkNone): if pk: drug get_object_or_404(Drug, pkpk) else: drug None if request.method POST: form DrugForm(request.POST, instancedrug) if form.is_valid(): drug form.save() return redirect(drugs:drug_list) else: form DrugForm(instancedrug) return render(request, drugs/drug_form.html, {form: form})删除操作我建议保守一点不要直接物理删除数据。医药系统的数据保存期限是有合规要求的物理删除后审计查证就无从谈起了。更好的做法是加一个 is_active 字段删除时只是把状态改掉这样底账还在界面上不再展示。如果后台管理确实需要物理删除到 Django Admin 里去操作就行了。3.2 库存流水与自动预警功能库存管理在这个系统里不是普通的增删改查而是要体现业务闭环。入库、出库操作不能只修改当前库存数字而是要生成流水记录。login_required def inventory_in(request): if request.method POST: drug_id request.POST.get(drug_id) quantity int(request.POST.get(quantity)) drug get_object_or_404(Drug, pkdrug_id) # 新增入库流水 InventoryRecord.objects.create( drugdrug, record_typein, quantityquantity, operatorrequest.user ) messages.success(request, f{drug.name} 入库 {quantity} {drug.unit} 成功) return redirect(inventory:record_list) drugs Drug.objects.all() return render(request, inventory/inventory_in.html, {drugs: drugs})出库的逻辑和入库类似但要额外检查库存是否足够。我在实际开发中写了一个库存检查函数专门用于判断当前库存是否满足出库数量。这个函数通过聚合求和库存流水表来实现保证数据一致性。from django.db.models import Sum def get_drug_stock(drug): 计算指定药品的当前库存 result InventoryRecord.objects.filter(drugdrug).aggregate( total_inSum(quantity, filterQ(record_typein)), total_outSum(quantity, filterQ(record_typeout)) ) total_in result[total_in] or 0 total_out result[total_out] or 0 return total_in - total_out库存预警的逻辑很简单给药品表加一个 min_stock 字段表示最低库存阈值。每次查询药品列表时比较当前库存和阈值小于阈值就高亮显示提醒。把预警写成一个独立的函数可以在多个地方复用不用重复写逻辑。3.3 用户权限与操作日志设计这个系统里用户分为管理员和普通员工用 Django 内置的权限系统就能实现。我在自定义 User 模型里加了 role 字段来区分角色视图层用 user.is_authenticated 和 user.role 来控权。但光控权不够医药系统对操作审计有要求谁在什么时间修改了哪个药品、做了入库还是出库都要能查。这部分我设计了一个操作日志表class OperationLog(models.Model): user models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, verbose_name操作人) action models.CharField(max_length50, verbose_name操作类型) detail models.TextField(verbose_name操作详情) ip_address models.CharField(max_length20, verbose_nameIP地址) created_at models.DateTimeField(auto_now_addTrue, verbose_name操作时间) class Meta: db_table operation_log verbose_name 操作日志 verbose_name_plural verbose_name在关键的增删改操作里加一行 OperationLog.objects.create(...) 就能完成记录。管理员可以查看日志列表普通员工看不到这个菜单。这样设计的成本极低但让系统一下子有了完整性和可信度。3.4 前端页面的处理技巧Django 的前端页面我主要用 Bootstrap 来写不需要复杂的前端工程化。写快速原型时我直接在基础模板里引入 Bootstrap 的 CDN 文件布局用栅格系统表格用样式类表单用表单组。模板继承是 Django 模板系统里很重要的特性把公共部分抽到 base.html 里子页面只写自己的内容块。!-- base.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}医药信息管理系统{% endblock %}/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.1.3/dist/css/bootstrap.min.css relstylesheet /head body nav classnavbar navbar-expand-lg navbar-dark bg-primary div classcontainer a classnavbar-brand href#医药信息管理系统/a div classcollapse navbar-collapse idnavbarNav ul classnavbar-nav li classnav-itema classnav-link href{% url dashboard %}首页/a/li li classnav-itema classnav-link href{% url drugs:drug_list %}药品管理/a/li li classnav-itema classnav-link href{% url inventory:record_list %}库存管理/a/li li classnav-itema classnav-link href{% url suppliers:supplier_list %}供应商管理/a/li li classnav-itema classnav-link href{% url sales:sale_list %}销售管理/a/li /ul ul classnavbar-nav ms-auto li classnav-itemspan classnav-link{{ request.user.username }}/span/li li classnav-itema classnav-link href{% url logout %}退出登录/a/li /ul /div /div /nav div classcontainer mt-4 {% if messages %} {% for message in messages %} div classalert alert-{{ message.tags }}{{ message }}/div {% endfor %} {% endif %} {% block content %}{% endblock %} /div /body /html列表页面的表格加几个字段就够用了药品名称、编码、厂家、规格、库存、价格、操作按钮。表单页面用 Django 表单渲染减少手写 HTML 的工作量也天然支持表单验证。4. 部署流程与常见问题速查4.1 本地运行和数据库迁移实操拿到源码之后第一步是搭建运行环境。我用的是虚拟环境方案避免依赖包冲突python -m venv venv source venv/bin/activate # Windows 下改为 venv\Scripts\activate pip install -r requirements.txtrequirements.txt 里的核心依赖是 Django 和 mysqlclient如果数据库用 MySQL如果用的是项目自带的 sqllite则不需要额外安装数据库驱动。settings.py 里对数据库做如下配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: medicine_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }如果本地没有 MySQL直接用 Django 默认的 SQLite 也可以跑起来在 settings.py 里把 ENGINE 改为 django.db.backends.sqlite3 即可不用做其他额外配置。SQLite 对课程设计来说完全够用等需要部署上线再切换 MySQL。4.2 部署到服务器的关键步骤本地跑通之后部署到服务器我用的是 Windows waitress nginx 的组合或者 Linux gunicorn nginx 的组合。前者比较适合 Windows 环境后者更通用。在 Linux 上部署的核心步骤如下首先收集静态文件Django 本身不负责处理静态文件的线上托管需要执行python manage.py collectstatic然后在 settings.py 里设置DEBUG False ALLOWED_HOSTS [你的域名或IP] STATIC_ROOT /path/to/staticfiles/再用 gunicorn 启动应用pip install gunicorn gunicorn medicine_system.wsgi:application --bind 0.0.0.0:8000最后配置 nginx 反向代理和静态文件服务。nginx 的配置片段大致长这样server { listen 80; server_name your_domain.com; location /static/ { alias /path/to/staticfiles/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }4.3 常见问题排查与避坑经验我在开发和部署过程中遇到最多的问题基本集中在下面这几类可以做个速查数据库连接报错、静态文件加载失败、部署后页面白屏、模板变量访问不到数据、时区导致的时间不对。数据库连接报错大部分是因为 MySQL 服务没启动或者账号密码错误。确认方法很简单先尝试用数据库客户端手动连接能连上再去排查 Django 配置。静态文件加载失败几乎是每个新手都会踩的坑。开发模式DEBUGTrue下要用 django.contrib.staticfiles 提供的静态文件服务模板里用 {% load static %} 和 {% static css/style.css %} 来引用静态文件而不是写死路径。部署后页面白屏有个很容易被忽略的原因ALLOWED_HOSTS 没有把域名或 IP 加进去Django 直接拒绝处理请求。看到 DisallowedHost 报错就知道是这个问题。模板里取不到数据的情况多半是 context 变量名和模板里写的不一致。Django 模板渲染对不存在的变量名不会直接报错而是渲染为空字符串肉眼排查起来特别费劲。我的办法是先在视图函数里打印 context确认数据确实传到模板了再去对比模板里的变量名。时区问题是在 settings.py 里设置 TIME_ZONE 和 USE_TZ。国内项目建议设置 TIME_ZONE Asia/Shanghai如果 USE_TZ TrueDjango 存储的是 UTC 时间模板显示时再转换成当前时区。如果不希望有这种转换逻辑直接 USE_TZ False统一用本地时间。4.4 源码阅读顺序与二次开发建议如果你拿到的是别人写好的源码不要一上来就全局搜索。我建议按下面的顺序去读第一步读 requirements.txt 和 README搞清楚项目依赖哪些第三方库、数据库用的什么、Python 版本要求。第二步读 settings.py了解项目配置和 app 注册情况。第三步读 urls.py通过路由表快速了解整个系统的功能入口。第四步读 models.py理解数据模型和表之间的关系。第五步读 views.py 和 templates理清业务逻辑和前端渲染的关系。这里还应该看一下项目中是否有 django 的自定义管理后台配置admin.py 里的配置能帮你快速了解到系统有哪些核心数据需要管理也方便你在开发阶段通过后台直接往库里加数据。二次开发的时候优先在现有 app 里扩展不要动不动就新建 app。如果要在药品表上加一个“药品分类”字段先看现有的 Drug 模型能不能直接加字段、做迁移而不是重新设计一张分类表把简单问题复杂化。当然如果分类确实有独立的业务属性比如分类名称、分类编码、上级分类那就值得单独建模型。5. 数据库脚本与文档编写要点5.1 SQL 脚本的生成与使用场景项目附件里通常包含一个 .sql 文件这是数据库的初始化脚本。Django 项目一般不需要手写 SQL直接用 migrate 生成表结构就行但考虑到课程设计提交时老师可能会直接查看 SQL 文件我会额外导出一份 SQL 脚本备用。导出 SQL 的命令python manage.py dumpdata data.json导出数据表结构可以这么操作# SQLite 数据库导出 sqlite3 db.sqlite3 .dump database.sql如果是 MySQL可以使用 mysqldump 命令导出。拿到 SQL 脚本后在文档中说明数据库的导入方式即可例如-- 创建数据库 CREATE DATABASE medicine_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 导入数据 USE medicine_db; SOURCE database.sql;5.2 系统说明文档怎么组织文档是课程设计报告中很重要的一部分也是很多人最容易忽视的部分。我提交这个项目时文档严格按照下面几个章节组织项目介绍、开发环境说明、系统功能分析、数据库设计、核心代码说明、系统运行步骤、测试报告、总结与展望。数据库设计部分要画出 E-R 图并把每张表的主要字段列成表格。核心代码说明部分不贴大段代码而是挑关键功能的实现思路和代码片段。运行步骤要写得足够傻瓜能让老师直接照做成功运行不让对方卡在环境配置这种环节上。文档的详略安排要合理不要流水账更不要大段贴没用的代码。我见过有些同学把模板里的 HTML 一次性贴了三四百行完全没有解释这种文档对读者来说没有意义。好的文档是能清晰回答“为什么这么设计”和“怎么运行起来”这两个问题的而不是简单罗列代码。5.3 课程设计答辩时的高频问题课程设计完成后答辩或演示环节老师通常会抓着几个关键点提问提前准备好这些问题的答案会给你增加不少印象分。第一个问题必然是这个系统采用了什么技术架构要能流畅说出 Django MTV 架构的含义M 是 Model 数据模型层负责与数据库交互T 是 Template 模板层负责页面展示V 是 View 视图层负责业务逻辑处理和数据传递。第二个问题是项目中的难点是什么你是怎么解决的我的回答是库存流水与库存实时计算的一致性解决方式是通过库存流水表记录每一次变动用聚合查询实时汇总当前库存保证数据可追溯、不会出现账实不符。第三个问题是这个系统相比传统人工管理有什么优势从效率提升、错误减少、数据追踪、库存预警方面来谈就可以这个比较直接。第四个问题涉及到系统能怎么扩展比如增加采购管理模块、药品有效期预警、报表导出这些方向都值得提说明你有继续迭代的意识。6. 写在最后的一点心得从项目立项到最终交付我最大的体会是这类信息管理系统的核心不在框架用得多花哨而在数据模型设计得是否扎实。一张合理的数据表结构能帮你省掉后面无数次的代码返工所以在数据库设计阶段多花时间推敲字段和外键关系比在视图层反复改逻辑要高效得多。如果你是在校学生拿着这份源码做课程设计建议不要只知道照抄运行而是把每个模块都过一遍逻辑能改一处小功能就自己改一处。哪怕只是把搜索的关键字改成厂家编号这个过程中对 Django 查询语法的理解都会上一个台阶。如果你是在职开发者这套项目的价值在于它演示了一个完整业务系统从零到一的开发思路。Django 的脚手架能力很强但真正的业务逻辑是脚手架给不了的需要你根据需求反复斟酌。最后一个实用小建议开发过程中务必定期提交代码用 Git 管理版本。医药信息管理系统这种项目改一版需求后代码基本就改了一半没有版本管理的话很容易翻车有了 Git 才能放心地大胆重构。