ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Django的医药信息管理系统设计与实现:从批号效期到库存预警全流程

基于Django的医药信息管理系统设计与实现:从批号效期到库存预警全流程 一套医药信息管理系统听起来好像就是“增删改查”但真做起来从药品批号管理到库存效期预警从进销存流程到用户权限控制每一块都有不少门道。我自己在课程设计和实际项目里反复打磨过这套基于Python和Django的方案源码、数据库、文档一条龙都整理过今天把核心思路和踩过的坑都摊开来讲。这套系统解决的不只是“管理药品信息”这么简单。医药流通有个特殊性药品不同于普通商品它有批号、有效期、生产日期、批准文号入库出库必须能追溯到每一批货。再加上近效期预警、库存不足提醒、权限分级比如店员不该看到进价这些需求堆在一起用Django做正好合适——自带的ORM能快速建表建模Admin后台可以当临时管理界面用第三方库又足够丰富做完之后写文档也好整理。适合谁来参考正在做课程设计、毕业设计的在校生刚接触Django想找一个完整业务练手的新手甚至小药房、诊所想搭内部管理工具的技术负责人这套思路都能直接落地。我下面会从方案拆解、数据库设计、核心模块实现、部署上线到问题排查一路讲下去尽量把每一个选择的理由都说清楚。1. 项目整体设计与技术选型1.1 为什么选Python Django而不是其他方案选型这件事很多人上来就纠结。我这个项目最初也犹豫过要不要用前后端分离Vue Spring Boot或者直接用Flask但最终锁定了Django原因是它在这类“管理系统”场景下几乎是为所欲为。第一Django自带ORM数据库操作不需要写一行SQL。医药系统有大量关联查询药品表关联供应商、入库单关联药品、销售单关联库存批次如果用原生SQL写光是联表就能写到手软。Django的ORM把模型定义清楚后查询就是Medicines.objects.filter(category抗生素)这种级别开发效率翻倍。第二Django内置Admin后台。在系统做出来之前我需要一个快速录入数据、验证模型的入口Django Admin直接注册模型就能用不用写一行前端代码。后期交付给别人演示的时候Admin后台也能充当一个“基础信息维护界面”省事。第三Django的MTV架构层次分明文档好写。做完之后要交文档按models、views、templates、urls四个层次拆开讲每一层干什么清清楚楚不用费劲解释业务逻辑塞在哪。Flask虽然轻但用户认证、CSRF保护、ORM、Admin这些都要自己搭或找第三方库在一套需要权限控制和数据管理的系统里性价比不高。前后端分离方案如果对接的是课程设计或自用项目等于多了一倍工作量没必要。1.2 医药信息管理系统的核心模块划分这个系统不是简单的“药品CRUD”。我按实际业务流程把它拆成了六个模块每个模块后面都有对应的业务逻辑支撑用户与权限管理登录、登出、修改密码、角色区分管理员、药师、店员。权限控制到“店员看不到采购价”这个级别。药品信息管理药品的通用名、商品名、剂型、规格、单位、生产厂家、批准文号、储存条件、有效期等。供应商管理供应商名称、联系人、电话、资质证号药品经营许可证号药品入库时关联到供应商。入库管理药品采购入库单、入库明细、批号、生产日期、有效期、入库数量、进货价。库存管理实时库存、批次库存、近效期预警比如剩余90天自动标黄、低库存预警。出库管理销售出库销售单、销售明细、从哪个批次扣减库存、销售价格、销售时间。这六个模块合在一起就形成了一个闭合的业务链路药品从供应商进来带着批号和效期进入库存出库时按批次先进先出扣减库存量实时变化后台还能看到近效期提醒。这个逻辑并不复杂但它覆盖了一个医药管理系统最核心的场景——批号和效期管理。1.3 源码结构规划没有规划的后端代码写到最后就是一团浆糊。我项目里的源码结构长这样拿到源码后第一件事应该先看目录medicine_system/ │ ├── manage.py ├── requirements.txt ├── db.sqlite3 ├── medicine_system/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── apps/ │ ├── users/ # 用户认证与角色管理 │ ├── medicines/ # 药品信息管理 │ ├── suppliers/ # 供应商管理 │ ├── stock_in/ # 入库管理 │ ├── stock_out/ # 出库管理销售 │ └── inventory/ # 库存查询与预警 │ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ │ ├── templates/ │ ├── base.html │ ├── users/ │ ├── medicines/ │ └── ... │ └── docs/ ├── 需求文档.md ├── 设计文档.md └── 部署说明.md为什么要拆成多app而不是全部塞在一个app里医药系统业务线多、模型之间关系复杂如果只建一个appmodels.py会膨胀到上千行views.py更是难维护。Django本身就是“可插拔app”的设计哲学按业务域拆分之后每个app都高内聚低耦合后面加新功能、修bug都清晰很多。2. 数据库设计与建模2.1 核心数据表设计思路数据库是整个系统的地基。我用Django默认的SQLite做开发和演示生产环境切到MySQL只要改settings配置。下面这几张表是系统里最核心的设计药品信息表 (Medicines)字段类型说明idIntegerField主键nameCharField药品通用名trade_nameCharField商品名categoryCharField药品分类specificationCharField规格如0.25g*24粒dosage_formCharField剂型片剂/胶囊/注射液unitCharField单位盒/瓶/袋manufacturerCharField生产厂家approval_numberCharField批准文号storage_conditionCharField储存条件常温/阴凉/冷藏created_atDateTimeField创建时间供应商表 (Suppliers)供应商名称、联系人、联系电话、地址、经营许可证号。为什么许可证号要做单独字段医药行业对供应商资质审查有硬性要求入库单要能追溯到货是从哪家供应商来的所以这个字段不能省。入库单表 (StockIn)入库单号、供应商外键、入库日期、操作员、备注。单号用“RK 年月日 当天流水号”的方式生成比如RK20250117001这个单号要唯一方便后面追溯。入库明细表 (StockInItem)入库单外键、药品外键、批号、生产日期、有效期、进货价、数量。批号和有效期是医药系统和其他进销存系统最大的区别必须放在明细表里。库存批次表 (StockBatch)药品外键、入库明细外键、批号、有效期、当前剩余数量。这张表的本质是“按批次存活的库存”批号相同但入库时间不同要分成两个批次来管理。出库单表 (StockOut)和出库明细表 (StockOutItem)销售单号SO 日期 流水、药品、对应库存批次、销售价、数量、出库时间、操作员。2.2 Django模型定义与迁移对应上面这些表Django模型的关键代码长这样。重点看外键关系和批号字段的处理from django.db import models from django.contrib.auth.models import User class Medicine(models.Model): name models.CharField(药品通用名, max_length100) trade_name models.CharField(商品名, max_length100, blankTrue) category models.CharField(药品分类, max_length50) specification models.CharField(规格, max_length100) dosage_form models.CharField(剂型, max_length20) unit models.CharField(单位, max_length10) manufacturer models.CharField(生产厂家, max_length200) approval_number models.CharField(批准文号, max_length100) storage_condition models.CharField(储存条件, max_length20) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.name} {self.specification} class Meta: db_table medicines class Supplier(models.Model): name models.CharField(供应商名称, max_length200) contact models.CharField(联系人, max_length50) phone models.CharField(联系电话, max_length20) address models.CharField(地址, max_length200, blankTrue) license_no models.CharField(经营许可证号, max_length100) class Meta: db_table suppliers class StockIn(models.Model): order_no models.CharField(入库单号, max_length30, uniqueTrue) supplier models.ForeignKey(Supplier, on_deletemodels.PROTECT) operator models.ForeignKey(User, on_deletemodels.PROTECT) created_at models.DateTimeField(auto_now_addTrue) remark models.TextField(备注, blankTrue) class Meta: db_table stock_in class StockInItem(models.Model): stock_in models.ForeignKey(StockIn, related_nameitems, on_deletemodels.CASCADE) medicine models.ForeignKey(Medicine, on_deletemodels.PROTECT) batch_no models.CharField(批号, max_length50) produce_date models.DateField(生产日期) expire_date models.DateField(有效期至) purchase_price models.DecimalField(进货价, max_digits10, decimal_places2) quantity models.IntegerField(数量) class Meta: db_table stock_in_items class StockBatch(models.Model): medicine models.ForeignKey(Medicine, on_deletemodels.PROTECT) stock_in_item models.ForeignKey(StockInItem, on_deletemodels.PROTECT) batch_no models.CharField(批号, max_length50) expire_date models.DateField(有效期至) remaining_quantity models.IntegerField(剩余数量) class Meta: db_table stock_batches这里有几个细节必须说明。外键的on_delete我用了PROTECT而不是CASCADE。原因很简单如果某个药品已经产生了入库记录、库存记录和销售记录这个药品就不允许被直接删除。医药系统里数据有追溯性要求物理删除一条药品记录会导致关联的库存和出入库记录全部断裂。相比之下PROTECT会在有引用时阻止删除强制你先处理关联数据这对业务是保护。批号字段为什么不直接挂在药品表上而是放在入库明细和库存批次里同一个药品每次进货的批号都不同批号和“某一批药品”绑定不能和药品本身绑定。这个设计如果不注意等做完库存统计就会发现批号互相覆盖往上追溯完全对不上。2.3 库存批次扣减逻辑这是整个系统最值得认真讲的一块。出库时怎么扣库存不能简单地从药品总数里减掉要按批次扣并且坚持“先进先出”。假设药品A有两个批次第一批批号20240101剩余30盒有效期2026年1月第二批批号20240601剩余20盒有效期2026年6月现在要出库25盒正确做法是先从第一批扣25盒第一批剩余5盒如果出库35盒则第一批30盒全部扣完再从第二批扣5盒。用Django ORM实现from django.db import transaction from django.utils import timezone from .models import StockBatch, StockOut, StockOutItem transaction.atomic def create_stock_out(order_no, operator, items): items: [{medicine_id: 1, quantity: 25, sale_price: 12.50}, ...] stock_out StockOut.objects.create( order_noorder_no, operatoroperator, created_attimezone.now() ) for item in items: medicine_id item[medicine_id] quantity item[quantity] remaining quantity # 查询该药品所有有库存的批次按有效期升序排列实现先进先出 batches StockBatch.objects.filter( medicine_idmedicine_id, remaining_quantity__gt0 ).order_by(expire_date, id) for batch in batches: if remaining 0: break take min(batch.remaining_quantity, remaining) batch.remaining_quantity - take batch.save() StockOutItem.objects.create( stock_outstock_out, medicine_idmedicine_id, stock_batchbatch, batch_nobatch.batch_no, quantitytake, sale_priceitem[sale_price] ) remaining - take if remaining 0: raise ValueError(f药品ID {medicine_id} 库存不足) return stock_outorder_by(expire_date)就是先进先出的核心把有效期最近的批次排最前面先扣最临期的药既符合业务实际也符合GSP对近效期药品的管理要求。transaction.atomic()也必须用。一个出库单要创建主表记录、扣减多个批次、写多条明细任何一步出错都要整体回滚否则会出现“出库单建了但库存没扣”或者“批次扣了但明细没生成”的脏数据。2.4 数据库同步与迁移实践经验拿到源码后第一步就是处理数据库。我用Django Migration管理表结构新增字段、改字段类型、建索引都走迁移文件。常见的坑是在已有数据的表上改字段比如把quantity从IntegerField改成PositiveIntegerField如果表里有负数就会迁移失败。我团队实际操作中有一条经验开发阶段如果数据不重要直接删掉db.sqlite3文件重新执行python manage.py migrate最快最省心。如果数据重要一定要先备份再做migration。生产环境如果用MySQL改表结构前先用SHOW TABLE STATUS看一下表大小大表加字段会导致锁表。作为副产物文档里我写了一段“数据库备份与恢复”的说明用Django自带的dumpdata命令就够了# 备份全部数据到JSON文件 python manage.py dumpdata --exclude auth.permission --exclude contenttypes backup.json # 恢复数据 python manage.py loaddata backup.json3. Django MTV架构实践3.1 MTV模式到底在说什么很多新手听到“Django的MTV模式”就懵其实说穿了很简单。MTV就是Model-Template-View它把一次请求拆成三层各管一段。Model管数据是数据库的翻译官。你定义模型类Django自动帮你建表你操作模型对象Django帮你翻译成SQL。Template管展示就是HTML页面里面能用Django模板语法写{{ 变量 }}和{% for %}循环把后端数据变成用户看到的页面。View管业务逻辑接收用户请求操作Model拿数据把数据塞给Template渲染最后把结果返回给浏览器。我在项目里写过一个“药品列表”的例子用户在浏览器访问/medicines/URL调度器找到对应的views函数views函数查询所有药品列表再把列表渲染到模板里用户就看到了一张完整表格。这个流程就是MTV它让数据、逻辑、展示彻底分开改页面样式不用动后端代码改业务逻辑不用碰HTML。MTV的作用说白了就是让写数据库的人和写页面的可以分工让项目结构清晰可维护让代码不至于堆成一团浆糊。3.2 URL路由与视图编写路由方面Django用的是urls.py里配置URLconf。比如药品模块的URL配置from django.urls import path from . import views urlpatterns [ path(list/, views.medicine_list, namemedicine_list), path(add/, views.medicine_add, namemedicine_add), path(edit/int:pk/, views.medicine_edit, namemedicine_edit), path(delete/int:pk/, views.medicine_delete, namemedicine_delete), path(detail/int:pk/, views.medicine_detail, namemedicine_detail), ]视图函数我建议统一用类视图还是函数视图这个项目里我混用了。简单页面用函数视图方便直接涉及表单提交和校验的地方用CreateView、UpdateView这些类视图能少写很多代码。比如药品新增用Django的ModelFormfrom django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from .models import Medicine from .forms import MedicineForm login_required def medicine_add(request): if request.method POST: form MedicineForm(request.POST) if form.is_valid(): form.save() return redirect(medicines:medicine_list) else: form MedicineForm() return render(request, medicines/medicine_form.html, {form: form})login_required装饰器是权限控制的第一步没有登录的用户访问这个URL会被自动重定向到登录页。这是Django自带的认证能力不需要自己写session判断这就是框架的价值。3.3 模板渲染与静态文件处理的坑模板这块最大的坑就是静态文件。我经常看到有人问“Django的static文件为什么显示不了”其实原因就那几样。正确的做法是在模板文件开头加{% load static %}然后图片、CSS、JS都用{% static ... %}来引用比如link relstylesheet href{% static css/base.css %}。设置里要配置STATIC_URL /static/开发模式下STATICFILES_DIRS [BASE_DIR / static]。DEBUGTrue的时候Django会帮你处理静态文件但一旦设置DEBUGFalse进入生产模式Django就不再管静态文件了必须让Nginx代理指向STATIC_ROOT目录。我踩过的一个坑是用EclipseVSCode写img标签时写完路径就急着刷新页面结果图片出不来后来才发现是模板缓存没过重启一下开发服务器就好了。3.4 查询与删除对象时容易犯的错误Django ORM的get和filter经常被弄混。get返回一个对象如果有多个结果就报MultipleObjectsReturned没有就报DoesNotExistfilter永远返回QuerySet哪怕只有一个结果也是一个对象的集合。所以“如果存在就删除”这种逻辑要么用get加异常捕获要么用filter().exists()先判断# 方式1try/except最直观 try: medicine Medicine.objects.get(idmedicine_id) medicine.delete() except Medicine.DoesNotExist: # 处理不存在的场景 pass # 方式2exists判断 if Medicine.objects.filter(idmedicine_id).exists(): Medicine.objects.filter(idmedicine_id).delete() # 方式3filter直接delete注意返回的是删除数量 deleted_count, _ Medicine.objects.filter(idmedicine_id).delete()还有一个常用技巧是get_object_or_404在视图里查不到对象直接返回404页面省去手写异常。4. 核心功能模块实现细节4.1 药品信息管理增删改查的细节处理药品的增删改查看着简单里面有很多细节需要注意。新增药品时批准文号要做唯一性校验。同一个批号、同一个规格的药品如果批准文号重复说明数据录入有问题。Django里的实现是在Model层面加uniqueTrue或者在表单里做clean_approval_number方法。修改药品时有一点要特别注意药品分类、剂型这些字段用什么类型。我最初用的是TextField后面做统计时发现“胶囊”“片剂”“注射液”这些值在数据库里五花八门有“胶襄”“胶囊剂”“capsule”统计报表根本没法看。后来改成了ChoiceField限定死枚举值录入时下拉选择彻底根治了脏数据问题。Django Admin后台注册模型也很简单from django.contrib import admin from .models import Medicine, Supplier admin.register(Medicine) class MedicineAdmin(admin.ModelAdmin): list_display (name, specification, category, manufacturer, approval_number) search_fields (name, trade_name, approval_number) list_filter (category, dosage_form)4.2 入库管理的完整流程入库模块的流程是填写入库单号、选择供应商、添加药品明细选药品、填批号、生产日期、有效期、进货价、数量、保存。这里最麻烦的是入库单号和批号的管理。入库单号我建议用时间戳加随机数的方案保证唯一性。批号不能说“随便填”它是追溯的抓手同一个批号在不同入库单里出现是正常的但同一个入库单里不能出现两个一模一样的批号加药品组合。入库后要把库存批次同步到StockBatch表这样库存模块才有数据可以扣减。在StockInItem保存时同步生成StockBatchtransaction.atomic def create_stock_in_items(stock_in, items_data): for item in items_data: medicine item[medicine] batch_no item[batch_no] # 先创建入库明细 detail StockInItem.objects.create( stock_instock_in, medicinemedicine, batch_nobatch_no, produce_dateitem[produce_date], expire_dateitem[expire_date], purchase_priceitem[purchase_price], quantityitem[quantity] ) # 如果同样的药品批号已经有库存批次累加数量 existing_batch StockBatch.objects.filter( medicinemedicine, batch_nobatch_no ).first() if existing_batch: existing_batch.remaining_quantity item[quantity] # 更新有效期可能相同批号但有效期不同以较晚的为准 if item[expire_date] existing_batch.expire_date: existing_batch.expire_date item[expire_date] existing_batch.save() else: StockBatch.objects.create( medicinemedicine, stock_in_itemdetail, batch_nobatch_no, expire_dateitem[expire_date], remaining_quantityitem[quantity] )一个容易错的地方是同一供应商同一天来了两批相同药品批号也一样理论上可以合并库存但如果批号一样、有效期不同就必须分开存。所以上面代码里的有效期更新逻辑要写得保守一点宁可多存一个批次也不能把不同有效期的货混在一起。4.3 库存预警近效期与低库存双预警库存预警是这个系统的“智能感”来源也是医药业务比较看重的点。实现思路很简单写一个查询函数扫描StockBatch表找出满足预警条件的记录在首页做展示。from datetime import timedelta from django.utils import timezone from .models import StockBatch def get_expiring_batches(days90): 近效期预警未来90天内过期的批次 today timezone.now().date() expire_limit today timedelta(daysdays) return StockBatch.objects.filter( remaining_quantity__gt0, expire_date__lteexpire_limit ).order_by(expire_date) def get_low_stock_medicines(threshold50): 低库存预警剩余总量低于阈值的药品 from django.db.models import Sum low_stock_ids StockBatch.objects.values(medicine_id).annotate( totalSum(remaining_quantity) ).filter(total__ltthreshold) return low_stock_ids近效期预警展示的时候把剩余天数也显示出来让使用者一眼看出哪个最紧急。低库存预警的阈值在设置里做成常量不用改代码就能调。这样的预警功能做得不用太复杂一个页面加两个表格就够了。真做成了整个系统的实用性立刻提升一个档次。4.4 用户角色与权限控制医药信息管理系统里权限控制不是花架子。店员录入销售单不应该看到采购价经理能看到全部经营数据管理员管理所有内容。Django自带的User模型配合Group权限就能覆盖这个需求。具体做法是定义两个分组——staff店员和manager经理。在视图层用装饰器或者Mixin控制访问权限from django.contrib.auth.decorators import login_required, user_passes_test def is_manager(user): return user.groups.filter(namemanager).exists() login_required user_passes_test(is_manager) def purchase_price_view(request): # 只有经理能查看采购价 ...模板层面也可以用{% if perms.medicines.change_medicine %}来控制某些按钮是否显示用最少代码实现最实用的权限效果不需要上Django-rest-framework那套复杂的权限体系。5. 项目部署与生产环境运行5.1 本地环境搭建与依赖安装拿到源码后本地跑起来通常几步就完成。先装Python 3.10以上版本然后用pip install -r requirements.txt安装依赖。requirements.txt要固化版本不能留裸依赖名否则过段时间Django大版本升级可能直接跑不起来。Django4.2.9 waitress3.0.0 Pillow10.2.0接着执行迁移、创建超级用户、启动开发服务器python manage.py migrate python manage.py createsuperuser python manage.py runserver浏览器访问http://127.0.0.1:8000用超级用户登录Admin后台把供应商和药品数据录进去系统就活了。创建app这个动作新手总会搞不明白。我项目里有6个app每建一个就要执行一次python manage.py startapp medicines然后记得去settings.py的INSTALLED_APPS里注册漏掉这一步模型建表、迁移、Admin注册全都白搭。5.2 Windows环境下用waitress Nginx部署Django自带的runserver只适合开发生产环境怎么办Linux上大家习惯用Gunicorn或uWSGI但Windows服务器上这两个都不太好使。我实测下来Windows上用waitress做WSGI服务器前面再挂一个Nginx做反向代理和静态文件处理是最稳的组合这也是我非常推荐的生产部署方案。waitress是纯Python写的WSGI服务器安装后启动非常简单pip install waitress waitress-serve --listen127.0.0.1:8000 medicine_system.wsgi:application这样Django应用就跑在8000端口了。Nginx配置反向代理把80端口的请求转发到8000静态文件直接由Nginx处理server { listen 80; server_name your_domain_or_ip; # 静态文件目录Django收集所有静态文件到这里 location /static/ { alias C:/path/to/your/project/staticfiles/; } # 媒体文件如果有上传的图片等 location /media/ { alias C:/path/to/your/project/media/; } # 其他请求全部转发给waitress 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; } }部署前必须执行python manage.py collectstatic把static目录下的文件全部收集到STATIC_ROOT指定的目录否则Nginx找不到静态文件。这个命令会复制所有app的静态文件和Django后台的静态文件是上线前必做的一步。用waitress部署的核心优势是干净、简单、不依赖系统服务管理器Windows计划任务或简单启动脚本就能守护进程。要开机自启写个run_server.bat脚本放启动文件夹就行。5.3 开发环境与生产环境的配置分离项目里的settings.py不能只写一套配置。开发时DEBUGTrue静态文件由Django处理数据库用SQLite生产时DEBUGFalse静态文件交给Nginx数据库换成MySQL还要设置ALLOWED_HOSTS。我常用方案是拆成settings_base.py、settings_dev.py、settings_prod.py三个文件通过环境变量切换。这样本地跑和服务器部署用同一份代码不用来回改配置。如果不拆至少也要把下面几项设置搞清楚# 生产环境必须设置为False DEBUG False # 生产环境必须设置否则报Bad Request ALLOWED_HOSTS [你的域名或IP] # 生产环境的数据库配置示例MySQL DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: medicine_db, USER: medicine_user, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }6. 常见问题排查与避坑指南6.1 静态文件加载不出来的排查清单这个问题在社群里被问爆了我把排查思路整理成一个“清单式”的操作流程模板开头有没有写{% load static %}没写就引用{% static xxx %}会直接报错或者路径不对。设置的STATICFILES_DIRS路径是否存在路径写错等于白配。浏览器缓存是不是旧版本F12打开开发者工具强制刷新或者禁用缓存再试。生产模式下有没有执行collectstatic没有执行Nginx自然找不到文件。Nginx的alias路径和你实际收集静态文件的路径是否一致这个最容易错路径差一个斜杠都会404。按顺序查完80%的问题都能解决。剩下的要么是权限问题文件目录没有读取权限要么是代理配置问题Nginx没重启。6.2 数据库操作常见的坑与处理坑一删除对象时数据不可逆。Django的delete()是直接物理删除没有二次确认。我在项目里处理“删除药品”时加了弹窗确认同时在视图层加了if request.method POST才允许删除防止GET请求误删。坑二外键关联导致删除失败。药品被库存、入库明细、出库明细引用了删除时Django会报ProtectedError。这个现象不是bug是设计使然。处理方法有两种如果业务上允许“逻辑删除”就在模型上加is_active字段默认True删除时改成False查询时默认过滤掉如果一定要物理删除就得先处理关联数据按先后顺序删除。坑三日期字段格式问题。用expire_date做预警查询时一定要注意是date对象还是datetime对象。timezone.now()返回的是datetimetimezone.now().date()才是date。两者比较时混用会报错或者永远查不到结果。6.3 Django查询优化避免N1问题用ORM写列表页的时候新手最容易踩“N1查询”的坑循环里查数据库药品数量多了之后页面响应变得非常慢。典型场景是在药品列表中显示每个药品的库存总量如果在循环里逐条查询# 反面案例每调一次total_quantity就查一次数据库 for medicine in medicines: total StockBatch.objects.filter(medicinemedicine).aggregate(...)优化方案是用select_related和prefetch_related一次查询把关联数据全捞出来from django.db.models import Sum, F medicines Medicine.objects.annotate( total_quantityCoalesce(Sum(stockbatch__remaining_quantity), 0) )这条查询翻译成SQL就是一次LEFT JOIN加GROUP BY数据库只跑一遍。6.4 表单提交报错与CSRF验证失败Django表单验证失败时经常看不出错在哪。调试方法很简单在模板里把form.errors输出出来就能看到每个字段具体的错误信息。千万不要在视图里print(form.errors)就完事用户根本看不到。还有一个高频问题POST表单在没有{% csrf_token %}的情况下提交会报403。新手做项目时最容易漏掉这个。正确做法是所有form methodpost都加上{% csrf_token %}。如果是前后端分离用AJAX提交就在请求头里带X-CSRFToken。7. 源码文档与二次开发指南7.1 拿到源码后如何快速上手源码解压后第一件事不是跑代码而是看README和目录结构。我的源码包附带的说明文档里有完整的运行步骤、默认账号密码和模块介绍。如果拿到的是别人写的代码按下面顺序快速建立认知先看models.py弄清楚数据库里有哪些表、表之间什么关系这是整个系统的骨架。再看urls.py把URL和功能模块对应起来知道每个页面走哪个视图。最后看views.py和templates/理解业务逻辑是怎么实现的。开发调试时建议用VSCode配置好Python环境之后直接F5就能断点调试。我见过不少新手在Django里用print排查问题效率太低Django的调试工具django-debug-toolbar装上SQL查询、模板渲染、请求耗时一眼看全强烈推荐。7.2 基于这个项目还能扩展哪些功能这套系统已经完成了一个医药信息管理的核心闭环但它还可以往以下方向扩展药品图片管理用Django的ImageField存药品图片界面更直观。过期药品报损流程增加报损单模块过期药品从库存中移出并留存记录。采购订单管理从“入库单”前再加一个“采购单”流程先申请采购再入库多一层审批。统计报表按日、月汇总销售数量和销售额生成Top10药品销售榜和供应商供货统计。将SQLite换成MySQL或PostgreSQL系统承载更大数据量并发能力更强。引入Redis做缓存和Session存储高并发下页面响应速度明显提升。API接口化改造成前后端分离架构用Django REST Framework提供RESTful API对接小程序或App。这些扩展方向不是空话每一个都能落到这套系统现有的数据模型上扩展成本和维护成本都比较低也说明当初选Django这个方向是选对了它的生态足够支撑你从一个小系统一路做到中大型应用。7.3 文档编写与交付经验一个完整的课程设计或项目交付文档和源码、数据库同等重要。我整理的文档目录一般包含三份需求文档说清楚这个系统要解决什么问题、有哪些功能模块、每个模块的基本流程。这是给评审老师或甲方看的重点讲“为什么做”和“做什么”。设计文档数据库设计ER图、表结构说明、系统架构图、核心模块的流程图和关键代码说明。这是给接手开发的人看的重点讲“怎么做”。部署与使用说明环境要求、安装步骤、默认账号、功能演示截图、常见问题。这是给最终使用者看的重点是“怎么跑起来、怎么用”。图片素材、表格内容以及前文提到的业务选择背后的理由都归纳在相应章节里。这样一套组合拳下来无论是课程答辩还是项目验收都有据可查。做这个项目的过程中我最深的体会是技术选型只是第一步真正花时间的是业务逻辑的处理——怎么关联批号怎么控制效期怎么在权限边界上做取舍。Django给了我很稳妥的地基让我把精力集中在“医药业务怎么实现”上而不是重复造轮子。如果你也在做类似的系统不妨从这套源码和数据库结构入手先把它跑通再根据自己的业务需要一点点改造这样的学习路径比从零开始要省力得多。最后再分享一个小技巧数据库里那些归一化的枚举值、状态标记一定要在文档里对应加一个“字典表”说明比如category字段的值有“抗生素”“心脑血管”“中成药”等分别代表什么。当时我补文档的时候把这个表列出来之后整个系统的可读性上升了一个台阶后续加功能、修bug都顺了很多。
返回列表