基于Django的仓库管理系统实战:ORM建模、事务锁与库存闭环
简介这份基于Django的仓库管理系统毕业设计资源面向计算机专业毕业生和课程设计者围绕管理员、仓库管理员、库管员、普通用户等多角色权限展开覆盖仓库信息维护、物资分类、供应商管理、采购入库、领料出库、库间调拨等完整业务链路有助于学习从需求分析到系统落地的开发流程。压缩包内共386个文件大小约12.26MB主要包含Python后端源码、Vue前端组件、SVG图标、JPG/PNG图片、CSS样式、SQL数据库脚本以及一键启动/安装的BAT脚本并附带论文设计文档文件类型多样目录结构清晰。目前已有64人学习系统内置用户登录日志、库存最低阈值预警、单据编号自动生成、出入库记录查询、Excel/PDF导出等功能体现工程化细节。通过项目可深入学习Django自定义权限控制与多角色鉴权、库存数量事务性更新、前后端数据交互等核心实现。配合数据库脚本与启动命令即可快速运行方便论文要点对照、毕业答辩演示及后续功能扩展。1. 用 Django 做仓库管理系统先分清论文系统和生产系统很多人在毕业论文里选“仓库管理系统”因为业务边界比电商、OA 都清楚物料进来、物料出去、库存还剩多少。但参考代码看多了会发现通病入库、出库、库存三个模块各写各的库存数量在页面上只是一个写死的数字根本没有和单据挂钩。这类系统演示时能点答辩时一问“这批库存是怎么来的”就断链。这篇内容按真实库存业务发生的顺序把 Django 建模、ORM 事务、admin 后台和核对脚本连起来讲从 Python 环境安装、django 创建 app 到查询优化都会提重点放在怎么把“进出存”这个闭环写对。适合正在准备基于 Django 的仓库管理系统设计论文、或想用 Python 快速搭一个可演示库存项目的开发者。2. 模型设计先行仓库管理系统的核心表结构与 Django ORM 字段选型2.1 进出存三张基础表不要一开始就铺二十张表先不要急着写视图和页面。一个能过答辩的仓库管理系统第一眼看的通常是 Django 模型表结构。你也不需要一开始就把后台做成 20 张表那样只会让外键关系绕成一团。我一般会先建四个模型物料主数据、库存快照、入库单、入库明细出库单和出库明细在结构上复用同一套设计。把进出存三块打通以后再按论文需要添加供应商、库位、领料部门这些维度表而不是先上维度表。物料主数据Product决定哪些物料能进单据单号字段决定单据怎么检索。库存快照Stock只保存当前数量不带流水。这样拆开有一个直接好处当你要回答“现在还剩多少”时不需要从几万条流水里Sum直接查Stock就行当你要回答“这批货什么时候进来、单价多少”时回查StockInItem和StockIn。把这两类查询拆到不同表是库存系统性能的基本保障也是论文里能写清楚的设计点。新建应用时先保证 Django 环境可用。常见的准备步骤是在项目目录下建虚拟环境、安装 Django、执行django-admin startproject然后创建 appmkdir django-warehouse cd django-warehouse python3 -m venv venv source venv/bin/activate pip install django django-admin startproject config . python manage.py startapp warehousevenv把 Python 依赖隔离在当前目录不会污染系统环境。这里用python3而不是python是因为 Linux 和 macOS 上默认的python可能指向旧版解释器Windows 下则可以直接用python -m venv venv。startapp warehouse会在项目里生成warehouse应用目录后面所有模型、表单、视图都放这里面。2.2 Django 模型字段参数如何影响建表和查询在warehouse/models.py中第一版可以写成这样from django.db import models class Product(models.Model): code models.CharField(物料编码, max_length32, uniqueTrue) # 唯一约束 name models.CharField(物料名称, max_length128) spec models.CharField(规格型号, max_length128, blankTrue) unit models.CharField(单位, max_length16) low_stock models.DecimalField(库存下限, max_digits12, decimal_places2, default0) def __str__(self): return f{self.code} {self.name} class Stock(models.Model): product models.OneToOneField(Product, on_deletemodels.CASCADE, related_namestock) quantity models.DecimalField(当前数量, max_digits16, decimal_places3, default0) updated_at models.DateTimeField(更新时间, auto_nowTrue)uniqueTrue是在数据库层建立唯一约束阻止重复物料编码这比在视图里先查后插更可靠。DecimalField用来存数量避免二进制浮点带来的累计误差decimal_places3给称重料留足余量。OneToOneField表示一个物料只有一行库存related_namestock让你在模板里写product.stock.quantity而不是product.stock_set.quantity。auto_nowTrue在每次save()时自动刷新更新时间如果论文要求保留首次创建时间加auto_now_addTrue字段。接着建入库单和入库明细class StockIn(models.Model): order_no models.CharField(入库单号, max_length32, uniqueTrue, db_indexTrue) supplier models.CharField(供应商, max_length128, blankTrue) created_at models.DateTimeField(入库时间, auto_now_addTrue) remark models.TextField(备注, blankTrue) class StockInItem(models.Model): stock_in models.ForeignKey(StockIn, on_deletemodels.CASCADE, related_nameitems) product models.ForeignKey(Product, on_deletemodels.PROTECT) quantity models.DecimalField(入库数量, max_digits16, decimal_places3) price models.DecimalField(单价, max_digits12, decimal_places2, default0) class Meta: indexes [models.Index(fields[product])]ForeignKey(StockIn)表示一个入库单有多个明细related_nameitems让视图可以直接写stock_in.items.all()取到行项目。on_deletemodels.PROTECT与CASCADE相反明细表里只要有记录就不允许删除产品主数据这样历史单据不会出现悬空物料。db_indexTrue给单号加普通索引按单号查询时避免全表扫描。Meta.indexes是 Django 2.0 以后推荐写法比在字段参数里写db_indexTrue更适合表达联合索引场景。下面这张表是答辩时最常见的字段提问点直接复制到论文“数据库设计”一节也没问题字段类型关键参数一句话解释Product.codeCharFielduniqueTrue, max_length32物料编码全局唯一Stock.quantityDecimalFieldmax_digits16, decimal_places3当前库存精度到千分位StockIn.order_noCharFielddb_indexTrue, uniqueTrue入库单号支撑检索StockInItem.productForeignKeyon_deletePROTECT明细关联物料禁止物理删物料StockInItem.quantityDecimalFieldmax_digits16, decimal_places3单据数量与库存计算映射写完模型后执行迁移python manage.py makemigrations warehouse python manage.py migratemakemigrations warehouse后面的warehouse是可选参数但建议写上。当项目里有多个 app 时不带 app 名会弹出交互式选择容易把迁移文件生成到错误位置。迁移之后如果需要登录 Django admin再执行python manage.py createsuperuser。3. 把核心业务写进 Django 视图入库、出库、库存扣减与事务3.1 用 Formset 接收一单多料入库单才真实实际入库很少是单料操作最常见的画面是送货单上列出五六种物料。页面上如果只放一个物料字段业务上就不成立。Django 自带的inlineformset_factory就是为父子表编辑设计的它同时生成父表表单和子表表单集在校验时两个一起通过、一起保存。在warehouse/forms.py里写from django import forms from .models import StockIn, StockInItem class StockInForm(forms.ModelForm): class Meta: model StockIn fields [order_no, supplier, remark] StockInItemFormSet forms.inlineformset_factory( StockIn, StockInItem, fields[product, quantity, price], extra1, can_deleteTrue, )inlineformset_factory第一个参数是父模型第二个是子模型fields只允许 product、quantity 和 price 被编辑防止外部提交把stock_in或id一起改掉。extra1表示页面上默认显示一行空明细can_deleteTrue允许用户勾选“删除”来移除已有明细后端对应生成DELETE字段。创建视图from django.shortcuts import redirect, render def stock_in_create(request): if request.method POST: # 校验并保存 form StockInForm(request.POST) formset StockInItemFormSet(request.POST) if form.is_valid() and formset.is_valid(): stock_in form.save() formset.instance stock_in formset.save() return redirect(stock_in_detail, pkstock_in.pk) else: form StockInForm() formset StockInItemFormSet() return render(request, warehouse/stock_in_form.html, { form: form, formset: formset, })这段代码的关键在formset.instance stock_in。表单集校验时还不知道父对象的id所以必须先保存父表拿到主键再把它赋给表单集的instance否则子表外键会是空值。redirect(stock_in_detail, pk...)走的是 Django 的 URL reverse如果 URL 名称不是stock_in_detail运行时会抛NoReverseMatch排查时先查urls.py里app_name和路由name是否一致。这是基于 Python 的仓库管理系统里最常见的一类配置错误。3.2 库存扣减与事务select_for_update 处理并发超卖入库单保存后还要更新库存这一步不能简单写stock.quantity item.quantity。两个用户同时提交不同单据时就可能出现“读 100 → 加 10 → 写 110”和“读 100 → 加 20 → 写 120”交错执行后提交的一方把前一次写入覆盖掉。Django 里没有跨进程互斥锁必须借助数据库事务和行锁。我一般会把“确认入库”写成独立的逻辑函数并在函数上加transaction.atomicfrom django.db import transaction from .models import Stock transaction.atomic def confirm_stock_in(stock_in): for item in stock_in.items.select_related(product): # 加锁读取当前库存行防止并发覆盖 stock, created Stock.objects.select_for_update().get_or_create( productitem.product, defaults{quantity: 0}, ) stock.quantity item.quantity stock.save(update_fields[quantity, updated_at])transaction.atomic让整个循环处在同一个事务里任何一条明细写入失败前面已经执行的操作也会回滚不会出现“入库单有了但库存没加”的半截状态。select_for_update()会在数据库层对这些行加排他锁另一个事务试图读取或更新同一物料时会阻塞直到前一个事务提交或回滚。get_or_create返回二元组(stock, created)created只在首次创建时是True后续都复用已有库存行。update_fields明确告诉 Django 只更新哪几个列避免更新整个对象的所有字段。出库逻辑和入库方向相反但要增加库存是否足够的校验transaction.atomic def confirm_stock_out(stock_out): for item in stock_out.items.select_related(product): stock Stock.objects.select_for_update().get(productitem.product) # 锁定后判断 if stock.quantity item.quantity: raise ValueError(f{item.product.code} 库存不足: {stock.quantity}) stock.quantity - item.quantity stock.save(update_fields[quantity, updated_at])这里先select_for_update()再比较数量顺序很重要。如果先读一遍数量、判断够后再锁两个请求会同时通过判断最终仍然超卖。除了锁还可以用F()表达式做数据库端自减from django.db.models import F Stock.objects.filter(pkstock.pk).update(quantityF(quantity) - item.quantity)F()不会把对象读进 Python而是生成数据库端的UPDATE warehouse_stock SET quantity quantity - 20 WHERE id ...在 MySQL 中相当于原地自减性能更好。但它的缺点是无法在同一个语句里拿到旧值做判断所以出库前仍然要先查一次库存或配合条件更新写两段逻辑。论文里把“行锁 事务”和“F()表达式”作为两种方案对比展示比只贴代码更有内容。4. 后台管理与查询优化Django Admin 界面美化、列筛选、select_related 与分页4.1 先配置 ModelAdmin把开箱即用的后台变成业务后台不需要引入额外前端框架。Django admin 自带完整的 CRUD若直接注册列表页只显示对象名筛选和搜索完全不可用。要让它像一个仓库后台而不是一堆Product object需要配置ModelAdmin。from django.contrib import admin from .models import Product, Stock, StockIn, StockInItem admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display (code, name, spec, unit, low_stock) # 列表页列顺序 list_filter (unit,) # 右侧侧栏筛选 search_fields (code, name) # 搜索框字段 list_per_page 20 admin.register(StockInItem) class StockInItemAdmin(admin.ModelAdmin): list_display (stock_in, product, quantity, price) autocomplete_fields (product,) # 下拉框变搜索框list_display里每个字段都是一列如果放__str__字符串以外的属性Django 会按字段顺序生成列头和值。list_filter只适合枚举字段像unit这种值不多的单位字段比较合适。search_fields会生成 or 条件支持icontains索引模糊查询。autocomplete_fields依赖关联模型在它的ModelAdmin里配置了search_fields否则不会启用。接着再把Stock注册进去并指定只读字段admin.register(Stock) class StockAdmin(admin.ModelAdmin): list_display (product, quantity, updated_at) readonly_fields (updated_at,) # 该字段详情页只读readonly_fields适合放系统维护的字段比如更新时间、创建人。它在列表页不生效但在 form 里不会出现在可编辑字段区能防止操作员直接从后台把库存改成任意数字。4.2 N1 查询列表页变慢的常见原因保存正式单据后进入列表页会看到“当前数量”和“更新时间”。如果每一行都去数据库单独查一次 product后果是一页 50 行结果执行 51 条 SQL。日志里这种“1 条主查询 N 条关联查询”的现象叫 N1论文答辩时也常被问。在StockAdmin中加入def get_queryset(self, request): return super().get_queryset(request).select_related(product)select_related对ForeignKey和OneToOneField有效它把子表字段 JOIN 到主查询里执行一条 SQL 就带上 product 信息。注意它不能替代list_display里的自定义方法如果要显示product.low_stock也要在ProductAdmin中设置search_fields或直接以双下划线传入字段。视图层同理。比如在确认入库的循环里我们已经用了select_related(product)所以item.product.code不会每次都发新查询。如果是反向关联例如Product.objects.prefetch_related(stockinitem_set)那必须用prefetch_related它对ManyToMany和反向外键做第二次查询后在 Python 里根据id分组不会触发 N1。stock_ins StockIn.objects.prefetch_related(items__product)prefetch_related只生效一次如果后续代码把一个对象的items.all()之外的新 queryset 再过滤那又会触发查询。这个“缓存不走过滤条件”的特性可以写进论文比直接说“我优化了查询”更经得起追问。4.3 按单号、物料编码、时间范围筛选与分页仓库查询常见的是按单号模糊搜索、按时间段筛选。Django ORM 的__icontains、__date__gte、__lte可以直接覆盖这些需求。写一个通用的列表视图from django.core.paginator import Paginator from django.db.models import Q from .models import StockIn def stock_in_list(request): qs StockIn.objects.all().order_by(-created_at) keyword request.GET.get(keyword, ).strip() start request.GET.get(start, ) end request.GET.get(end, ) if keyword: qs qs.filter(Q(order_no__icontainskeyword) | Q(supplier__icontainskeyword)) if start: qs qs.filter(created_at__date__gtestart) if end: qs qs.filter(created_at__date__lteend) paginator Paginator(qs, 20) page_obj paginator.get_page(request.GET.get(page, 1)) return render(request, warehouse/stock_in_list.html, {page_obj: page_obj})Q对象用来把两个搜索条件包成一个 or 查询如果不用Q两个filter是 and 关系查不到“单号或供应商”的总结果。created_at__date__gte表示按日期部分比较大于等于括号里传入YYYY-MM-DD格式字符串即可。order_by(-created_at)保证新单在前分页稳定。关于删除仓库系统最常见的设计不是物理delete()而是加一个cancelled布尔字段。queryset.delete()在 Django 中会级联到关联明细如果历史数据已经被报表引用物理删除会造成账实不一致。在StockIn模型里加is_cancelled models.BooleanField(defaultFalse)列表页查询时默认过滤掉is_cancelledTrue即可实现前台不可见、历史可追溯。要删除对象时可以写StockIn.objects.filter(...).update(is_cancelledTrue)如果确实要全量删除StockIn.objects.all().delete()才会触发数据库级删除。Django 执行查询删除对象的两种方式在论文里都应写明用途。5. 用数据库约束、管理命令和 CSV 导出做库存核对5.1 数据库层挡住负库存负库存是最容易被答辩老师抓的漏洞。只靠前端校验不可靠直接把规则放到数据库层更稳。在Stock模型里加CheckConstraintclass Stock(models.Model): product models.OneToOneField(Product, on_deletemodels.CASCADE, related_namestock) quantity models.DecimalField(当前数量, max_digits16, decimal_places3, default0) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: constraints [ models.CheckConstraint(checkmodels.Q(quantity__gte0), namestock_quantity_non_negative) ]CheckConstraint会在迁移时生成数据库级 check 约束任何让quantity变负的UPDATE都会被数据库拒绝。name是约束名不能和已有索引重名。5.2 可演示的对账命令与 CSV 导出论文验收时最好能现场跑一段命令。Django 自定义 management command 不需要打开页面就能执行。在warehouse/management/commands/check_stock.py中写from django.core.management.base import BaseCommand from django.db.models import Sum from warehouse.models import Stock, StockInItem class Command(BaseCommand): help 校验当前库存与入库汇总是否一致 def handle(self, *args, **options): for stock in Stock.objects.select_related(product).all(): in_sum StockInItem.objects.filter( productstock.product ).aggregate(totalSum(quantity))[total] or 0 if stock.quantity ! in_sum: self.stdout.write(self.style.WARNING( f{stock.product.code} 库存{stock.quantity} 入库合计{in_sum} ))aggregate返回字典无记录时Sum返回None所以后面要接or 0。执行python manage.py check_stock所有差异都会打印出来这个动作在答辩现场比截图更可信。导出 CSV 时要注意 Excel 对 UTF-8 的识别先写入 BOMimport csv from django.http import HttpResponse def export_stock_csv(request): response HttpResponse(content_typetext/csv; charsetutf-8) response[Content-Disposition] attachment; filenamestock.csv response.write(\ufeff) writer csv.writer(response) writer.writerow([物料编码, 物料名称, 当前库存]) for stock in Stock.objects.select_related(product).all(): writer.writerow([stock.product.code, stock.product.name, stock.quantity]) return response\ufeff是 Unicode 字节序标记写入后 Excel 才能正确识别中文。Content-Disposition设为attachment时浏览器下载改为inline则在页面打开。若给这个视图加start和end参数查询时用updated_at__date__gte与__lte过滤导出的就是指定时间段库存快照。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →