news 2026/9/11 23:27:23

基于Django的仓库管理系统实战:ORM建模、事务锁与库存闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于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就行;当你要回答“这批货什么时候进来、单价多少”时,回查StockInItemStockIn。把这两类查询拆到不同表,是库存系统性能的基本保障,也是论文里能写清楚的设计点。

新建应用时,先保证 Django 环境可用。常见的准备步骤是在项目目录下建虚拟环境、安装 Django、执行django-admin startproject,然后创建 app:

mkdir django-warehouse && cd django-warehouse python3 -m venv venv source venv/bin/activate pip install django django-admin startproject config . python manage.py startapp warehouse

venv把 Python 依赖隔离在当前目录,不会污染系统环境。这里用python3而不是python,是因为 Linux 和 macOS 上默认的python可能指向旧版解释器;Windows 下则可以直接用python -m venv venvstartapp warehouse会在项目里生成warehouse应用目录,后面所有模型、表单、视图都放这里面。

2.2 Django 模型字段参数如何影响建表和查询

warehouse/models.py中,第一版可以写成这样:

from django.db import models class Product(models.Model): code = models.CharField('物料编码', max_length=32, unique=True) # 唯一约束 name = models.CharField('物料名称', max_length=128) spec = models.CharField('规格型号', max_length=128, blank=True) unit = models.CharField('单位', max_length=16) low_stock = models.DecimalField('库存下限', max_digits=12, decimal_places=2, default=0) def __str__(self): return f'{self.code} {self.name}' class Stock(models.Model): product = models.OneToOneField(Product, on_delete=models.CASCADE, related_name='stock') quantity = models.DecimalField('当前数量', max_digits=16, decimal_places=3, default=0) updated_at = models.DateTimeField('更新时间', auto_now=True)

unique=True是在数据库层建立唯一约束,阻止重复物料编码,这比在视图里先查后插更可靠。DecimalField用来存数量,避免二进制浮点带来的累计误差;decimal_places=3给称重料留足余量。OneToOneField表示一个物料只有一行库存,related_name='stock'让你在模板里写product.stock.quantity而不是product.stock_set.quantityauto_now=True在每次save()时自动刷新更新时间;如果论文要求保留首次创建时间,加auto_now_add=True字段。

接着建入库单和入库明细:

class StockIn(models.Model): order_no = models.CharField('入库单号', max_length=32, unique=True, db_index=True) supplier = models.CharField('供应商', max_length=128, blank=True) created_at = models.DateTimeField('入库时间', auto_now_add=True) remark = models.TextField('备注', blank=True) class StockInItem(models.Model): stock_in = models.ForeignKey(StockIn, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.PROTECT) quantity = models.DecimalField('入库数量', max_digits=16, decimal_places=3) price = models.DecimalField('单价', max_digits=12, decimal_places=2, default=0) class Meta: indexes = [models.Index(fields=['product'])]

ForeignKey(StockIn)表示一个入库单有多个明细,related_name='items'让视图可以直接写stock_in.items.all()取到行项目。on_delete=models.PROTECTCASCADE相反:明细表里只要有记录,就不允许删除产品主数据,这样历史单据不会出现悬空物料。db_index=True给单号加普通索引,按单号查询时避免全表扫描。Meta.indexes是 Django 2.0 以后推荐写法,比在字段参数里写db_index=True更适合表达联合索引场景。

下面这张表是答辩时最常见的字段提问点,直接复制到论文“数据库设计”一节也没问题:

字段类型关键参数一句话解释
Product.codeCharFieldunique=True, max_length=32物料编码,全局唯一
Stock.quantityDecimalFieldmax_digits=16, decimal_places=3当前库存,精度到千分位
StockIn.order_noCharFielddb_index=True, unique=True入库单号,支撑检索
StockInItem.productForeignKeyon_delete=PROTECT明细关联物料,禁止物理删物料
StockInItem.quantityDecimalFieldmax_digits=16, decimal_places=3单据数量,与库存计算映射

写完模型后执行迁移:

python manage.py makemigrations warehouse python manage.py migrate

makemigrations 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'], extra=1, can_delete=True, )

inlineformset_factory第一个参数是父模型,第二个是子模型;fields只允许 product、quantity 和 price 被编辑,防止外部提交把stock_inid一起改掉。extra=1表示页面上默认显示一行空明细,can_delete=True允许用户勾选“删除”来移除已有明细,后端对应生成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', pk=stock_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.pyapp_name和路由name是否一致。这是基于 Python 的仓库管理系统里最常见的一类配置错误。

3.2 库存扣减与事务:select_for_update 处理并发超卖

入库单保存后还要更新库存,这一步不能简单写stock.quantity += item.quantity。两个用户同时提交不同单据时,就可能出现“读 100 → 加 10 → 写 110”和“读 100 → 加 20 → 写 120”交错执行,后提交的一方把前一次写入覆盖掉。Django 里没有跨进程互斥锁,必须借助数据库事务和行锁。

我一般会把“确认入库”写成独立的逻辑函数,并在函数上加@transaction.atomic

from 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( product=item.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(product=item.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(pk=stock.pk).update(quantity=F('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 N+1 查询:列表页变慢的常见原因

保存正式单据后,进入列表页会看到“当前数量”和“更新时间”。如果每一行都去数据库单独查一次 product,后果是:一页 50 行,结果执行 51 条 SQL。日志里这种“1 条主查询 + N 条关联查询”的现象叫 N+1,论文答辩时也常被问。

StockAdmin中加入:

def get_queryset(self, request): return super().get_queryset(request).select_related('product')

select_relatedForeignKeyOneToOneField有效,它把子表字段 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分组,不会触发 N+1。

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__icontains=keyword) | Q(supplier__icontains=keyword)) if start: qs = qs.filter(created_at__date__gte=start) if end: qs = qs.filter(created_at__date__lte=end) 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(default=False),列表页查询时默认过滤掉is_cancelled=True,即可实现前台不可见、历史可追溯。要删除对象时可以写StockIn.objects.filter(...).update(is_cancelled=True);如果确实要全量删除,StockIn.objects.all().delete()才会触发数据库级删除。Django 执行查询删除对象的两种方式,在论文里都应写明用途。

5. 用数据库约束、管理命令和 CSV 导出做库存核对

5.1 数据库层挡住负库存

负库存是最容易被答辩老师抓的漏洞。只靠前端校验不可靠,直接把规则放到数据库层更稳。在Stock模型里加CheckConstraint

class Stock(models.Model): product = models.OneToOneField(Product, on_delete=models.CASCADE, related_name='stock') quantity = models.DecimalField('当前数量', max_digits=16, decimal_places=3, default=0) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: constraints = [ models.CheckConstraint(check=models.Q(quantity__gte=0), name='stock_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( product=stock.product ).aggregate(total=Sum('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 的识别,先写入 BOM:

import csv from django.http import HttpResponse def export_stock_csv(request): response = HttpResponse(content_type='text/csv; charset=utf-8') response['Content-Disposition'] = 'attachment; filename="stock.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则在页面打开。若给这个视图加startend参数,查询时用updated_at__date__gte__lte过滤,导出的就是指定时间段库存快照。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 23:27:07

mise cache 缓存管理命令详解:清理、定位、修剪与任务缓存检查

mise cache 缓存管理命令详解&#xff1a;清理、定位、修剪与任务缓存检查 【免费下载链接】mise dev tools, env vars, task runner 项目地址: https://gitcode.com/GitHub_Trending/mi/mise mise cache 是 mise 中用于统一管理各类缓存&#xff08;工具元数据缓存、环…

作者头像 李华
网站建设 2026/9/11 23:24:53

SWOT卫星数据反演瞬时河流流量的物理建模方法

简介&#xff1a;本资源是一套基于SWOT卫星遥感观测数据反演瞬时河流流量的MATLAB实现方案&#xff0c;面向计算机、电子信息工程及应用数学等专业的本科生与研究生&#xff0c;适用于课程设计、期末大作业及毕业设计等实践场景。代码采用参数化编程范式&#xff0c;核心算法模…

作者头像 李华
网站建设 2026/9/11 23:22:52

STM32双结点CAN控制实战:硬件搭建、位时序计算与Bus Off排查

把两个STM32控制板用CAN总线连起来&#xff0c;让其中一个发指令&#xff0c;另一个收到后执行控制动作&#xff0c;再把状态回传&#xff0c;这就是“CAN双结点控制”这个项目最典型的形态。我在开发板和实验室工装上都搭过这套东西&#xff1b;表面上只是两根差分线&#xff…

作者头像 李华
网站建设 2026/9/11 23:22:32

Findings: Dark Mode Implementation

Findings: Dark Mode Implementation 【免费下载链接】planning-with-files Persistent file-based planning for AI coding agents and long-running tasks. Crash-proof markdown plans, session recovery after /clear and compaction, per-turn re-injection against conte…

作者头像 李华