ARTICLE DETAIL

资讯详情

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

Python+Django仓库管理系统实战:从模型设计到库存并发扣减

Python+Django仓库管理系统实战:从模型设计到库存并发扣减 简介这是一套面向高校计算机相关专业学生与初学者的仓库管理系统完整项目源码基于Python与Django后端、Vue.js前端构建采用B/S架构可直接用于毕业设计或课程设计选题也适合作为全栈入门练手项目。压缩包共390个文件约20.62MB其中36个py文件承载后端业务逻辑15个vue与34个ts文件构成前端页面与交互另有大量jpeg、jpg、png等图片资源用于界面展示json、md、less等文件辅助配置与样式管理目录分为server后端与web前端两部分结构清晰。功能覆盖商品管理、分类管理、用户管理、日志管理及系统信息模块并附带演示地址与账号方便快速预览效果。目前已有387人学习下载读者可借此掌握Django接口开发、Vue组件化搭建、MySQL数据建模与前后端联调思路是一份可直接运行、便于二次修改的实战型参考方案。1. 从一份 Django 仓库管理系统源码说起它到底能解决什么很多计算机专业的同学在毕业设计或课程设计选题时都会碰到「仓库管理系统」这个题目。它看起来平平无奇但真正动手才发现库存怎么扣减、入库出库怎么记录、多角色权限怎么分、并发下库存会不会扣成负数——这些问题一个比一个具体。而pythondjango仓库管理系统这套组合恰好是当前高校选题里落地成本最低、可讲的技术点又足够多的方案之一。Python 负责业务逻辑Django 自带 ORM、Admin 后台和用户认证体系一套下来能把「进销存」的核心闭环跑通。这篇文章面向两类人一是刚拿到这个题目、不知道从哪下手的新手二是已经跑通了基础 CRUD、但想搞清楚库存并发、权限设计和数据一致性怎么处理的同学。我会按「环境搭建 → 数据模型设计 → 核心业务实现 → 踩坑排查 → 进阶技巧」的顺序把一套可复现的仓库管理系统拆开讲清楚。你不需要有 Django 项目实战经验但需要会基本的 Python 语法和命令行操作。2. 环境搭建与项目骨架从 python 安装到 django 创建 app2.1 版本选择与依赖安装Django 的版本兼容性是个老生常谈的问题。截至我写这篇内容时Django 4.2 LTS 是长期支持版本Python 3.10 或 3.11 都能稳定运行。如果你用的是 Python 3.12建议 Django 选 5.0 以上。不要用 Python 3.7 配 Django 5.x会直接报ImportError。安装流程本身不复杂但新手最容易在虚拟环境这一步翻车。我一般会强制用 venv避免全局包污染# 创建项目目录并进入 mkdir warehouse_sys cd warehouse_sys # 创建虚拟环境Windows 用 python -m venv venv python3 -m venv venv # 激活虚拟环境 source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装 Django 和数据库驱动 pip install django4.2.11 pip install mysqlclient # 如果用 MySQL # 或者用 SQLiteDjango 自带支持不需要额外安装这里有个参数说明mysqlclient在 Windows 上编译经常失败替代方案是pymysql但需要在__init__.py里加pymysql.install_as_MySQLdb()。如果只是做课程设计SQLite 完全够用省去数据库配置的麻烦。2.2 创建项目与 app 的目录规划Django 的startproject和startapp是两个不同层级的命令很多人第一次会搞混。项目是容器app 是功能模块。仓库管理系统我一般拆成四个 appgoods商品、inventory库存、orders出入库单、accounts用户角色。# 创建 Django 项目 django-admin startproject warehouse . # 创建四个功能 app python manage.py startapp goods python manage.py startapp inventory python manage.py startapp orders python manage.py startapp accounts执行完后目录结构应该是warehouse/settings.py和四个 app 文件夹平级。接着在settings.py的INSTALLED_APPS里注册这四个 app并在DATABASES里配置数据库连接。如果用 SQLite默认配置不用改如果用 MySQL需要提前建好库# settings.py 关键配置片段 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, goods, inventory, orders, accounts, ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: warehouse_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }utf8mb4这个字符集必须显式指定否则商品名称里如果有特殊符号或 emoji入库时会报Incorrect string value错误。这是血泪经验别问我怎么知道的。2.3 数据库迁移与 Admin 后台初始化配置好之后执行迁移命令生成 Django 内置表python manage.py makemigrations python manage.py migrate # 创建超级管理员用于登录 Admin 后台 python manage.py createsuperusermakemigrations是根据模型变化生成迁移文件migrate才是真正执行 SQL 建表。很多人改了模型只跑migrate发现表没变就是因为漏了makemigrations。跑完之后访问http://127.0.0.1:8000/admin/能看到 Django 自带的用户和权限管理界面说明环境通了。3. 数据模型设计商品、库存、出入库单怎么建表3.1 商品表与分类表的外键关系仓库管理系统的核心数据模型其实就三张表商品、库存、出入库记录。商品表存基础信息库存表存实时数量出入库单存流水。分类表是可选的但建议加上因为毕业设计答辩时老师大概率会问「商品分类怎么管理」。# goods/models.py from django.db import models class Category(models.Model): name models.CharField(分类名称, max_length64, uniqueTrue) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namechildren ) class Meta: db_table goods_category verbose_name 商品分类 def __str__(self): return self.name class Goods(models.Model): STATUS_CHOICES ( (0, 下架), (1, 在售), ) name models.CharField(商品名称, max_length128, db_indexTrue) sku models.CharField(商品编码, max_length64, uniqueTrue) category models.ForeignKey( Category, on_deletemodels.PROTECT, verbose_name所属分类 ) price models.DecimalField(单价, max_digits10, decimal_places2) status models.SmallIntegerField(状态, choicesSTATUS_CHOICES, default1) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table goods_goods verbose_name 商品 indexes [models.Index(fields[sku, status])] def __str__(self): return f{self.sku} - {self.name}这里有几个设计决策值得说清楚。price用DecimalField而不是FloatField因为浮点数在金额计算上会出现精度丢失比如0.1 0.2 ! 0.3。category的外键用PROTECT而不是CASCADE意思是分类下还有商品时不允许删除分类避免误操作导致数据丢失。sku加了uniqueTrue和联合索引因为出入库操作会频繁按 SKU 查询。3.2 库存表与并发扣减的字段设计库存表是最容易出问题的地方。新手常见的做法是直接在商品表上加一个stock字段简单是简单但没法记录库存变更历史也没法做多仓库支持。我一般会单独建一张库存表并且加上乐观锁需要的版本号字段。# inventory/models.py from django.db import models from goods.models import Goods class Inventory(models.Model): goods models.OneToOneField( Goods, on_deletemodels.CASCADE, related_nameinventory ) quantity models.IntegerField(库存数量, default0) warn_threshold models.IntegerField(预警阈值, default10) version models.IntegerField(版本号, default0) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table inventory_inventory verbose_name 库存 def __str__(self): return f{self.goods.name}: {self.quantity}OneToOneField保证一个商品只有一条库存记录。version字段是给乐观锁用的后面讲并发扣减时会用到。warn_threshold是库存预警阈值低于这个值就在后台标红提醒。3.3 出入库单的表结构与状态流转出入库单我拆成主表和明细表两张。主表记录单据头信息单号、类型、操作人、状态明细表记录每个商品的出入库数量。这种主子表结构是 ERP 系统的标准做法答辩时也能体现你对业务的理解。# orders/models.py from django.db import models from django.conf import settings from goods.models import Goods class StockOrder(models.Model): ORDER_TYPES ((1, 入库), (2, 出库)) STATUS ((0, 待审核), (1, 已审核), (2, 已作废)) order_no models.CharField(单号, max_length32, uniqueTrue) order_type models.SmallIntegerField(类型, choicesORDER_TYPES) status models.SmallIntegerField(状态, choicesSTATUS, default0) operator models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, verbose_name操作人 ) remark models.CharField(备注, max_length255, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table orders_stock_order ordering [-created_at] class StockOrderItem(models.Model): order models.ForeignKey( StockOrder, on_deletemodels.CASCADE, related_nameitems ) goods models.ForeignKey(Goods, on_deletemodels.PROTECT) quantity models.IntegerField(数量) class Meta: db_table orders_stock_order_item状态流转设计成「待审核 → 已审核 → 已作废」三态。只有「已审核」的入库单才会真正增加库存「已审核」的出库单才会扣减库存。这个设计的好处是审核环节可以人工确认避免误操作直接改库存。作废的单据保留记录但不影响库存方便追溯。4. 核心业务实现入库、出库与库存扣减的完整链路4.1 入库单创建与审核的视图逻辑入库流程分两步创建单据写主表和明细表状态为待审核审核单据改状态为已审核同时增加库存。这两步必须放在同一个数据库事务里否则审核成功但库存没加上数据就不一致了。# orders/views.py from django.db import transaction from django.db.models import F from rest_framework.decorators import api_view from rest_framework.response import Response from .models import StockOrder, StockOrderItem from inventory.models import Inventory import time api_view([POST]) transaction.atomic def audit_order(request, order_id): 审核出入库单同时更新库存 try: order StockOrder.objects.select_for_update().get( idorder_id, status0 ) except StockOrder.DoesNotExist: return Response({code: 404, msg: 单据不存在或已审核}) items order.items.select_related(goods).all() for item in items: inv, _ Inventory.objects.select_for_update().get_or_create( goodsitem.goods ) if order.order_type 1: # 入库 inv.quantity F(quantity) item.quantity else: # 出库 if inv.quantity item.quantity: raise ValueError(f{item.goods.name} 库存不足) inv.quantity F(quantity) - item.quantity inv.version 1 inv.save() order.status 1 order.save() return Response({code: 0, msg: 审核成功})这段代码有几个关键点。select_for_update()是数据库行锁配合transaction.atomic使用能防止并发审核同一张单据。F(quantity) item.quantity是 Django 的 F 表达式它直接在数据库层面做加减避免「读-改-写」的竞态条件。如果不用 F 表达式两个请求同时读到库存 10各自加 5最后结果可能是 15 而不是 20。4.2 出库时的库存校验与乐观锁兜底上面的代码用了悲观锁select_for_update在高并发下会阻塞。如果你的场景并发量不大这样写没问题。但如果想更优雅一点可以用乐观锁做兜底。乐观锁的思路是更新时检查版本号如果版本号变了说明有人改过就重试。# inventory/services.py from django.db.models import F from .models import Inventory def deduct_stock(goods_id, quantity, max_retry3): 乐观锁扣减库存失败自动重试 for _ in range(max_retry): inv Inventory.objects.get(goods_idgoods_id) if inv.quantity quantity: raise ValueError(库存不足) # 带版本号条件更新返回受影响行数 updated Inventory.objects.filter( idinv.id, versioninv.version ).update( quantityF(quantity) - quantity, versionF(version) 1 ) if updated: return True # updated 0 说明版本号被改过重试 raise RuntimeError(库存扣减失败请重试)filter(idinv.id, versioninv.version).update(...)这一句是乐观锁的核心。如果两个请求同时拿到 version5第一个更新成功后 version 变成 6第二个请求的version5条件就不满足updated返回 0触发重试。这种方案不需要数据库行锁吞吐量更高但需要业务层处理重试逻辑。4.3 库存预警与定时任务库存预警是个加分项。实现方式很简单每次库存变更后检查是否低于阈值低于就写一条预警记录。但更常见的做法是用 Django 的management command配合 crontab 定时扫描。# inventory/management/commands/check_warn.py from django.core.management.base import BaseCommand from inventory.models import Inventory class Command(BaseCommand): help 扫描库存低于阈值的商品 def handle(self, *args, **options): warns Inventory.objects.filter( quantity__ltF(warn_threshold) ).select_related(goods) for inv in warns: self.stdout.write( f[预警] {inv.goods.name} 当前库存 {inv.quantity} f阈值 {inv.warn_threshold} ) self.stdout.write(f共 {warns.count()} 条预警)执行python manage.py check_warn就能跑一次扫描。如果要定时执行Linux 下用 crontab 加一行0 9 * * * cd /path python manage.py check_warn每天早上九点跑一次。Windows 下可以用任务计划程序。5. 避坑与排查Django 仓库系统最常见的 5 个翻车现场5.1 迁移文件冲突导致 migrate 失败现象执行python manage.py migrate时报django.db.migrations.exceptions.NodeNotFoundError或者提示某个迁移文件依赖不存在。原因多人协作时两个人同时改了同一个模型并各自生成了迁移文件合并代码后迁移文件的依赖关系断了。或者手动删过migrations目录下的文件但数据库里的django_migrations表还记录着已执行。解决先python manage.py showmigrations看哪些迁移没执行。如果是依赖断裂找到冲突的两个迁移文件手动调整dependencies列表。如果是数据库记录和文件对不上删掉数据库里对应的记录行或者直接删库重建课程设计阶段数据不重要的话这是最快的办法。5.2 出库时库存扣成负数现象并发测试时发现某个商品库存变成了负数但代码里明明有if inv.quantity quantity的判断。原因判断和扣减之间有时间窗口。两个请求同时读到库存 10都判断 10 8 通过然后各自扣 8结果变成 -6。这是典型的竞态条件。解决用select_for_update()加行锁或者用 F 表达式在数据库层面做原子扣减并在 SQL 的 WHERE 条件里带上quantity xxx。最稳妥的是两者结合Inventory.objects.filter(idinv.id, quantity__gtequantity).update(quantityF(quantity) - quantity)根据返回的受影响行数判断是否成功。5.3 Admin 后台商品列表加载慢现象商品数量到几千条后Django Admin 的商品列表页打开要好几秒。原因Admin 列表默认会显示外键关联对象的__str__每条记录都触发一次额外查询也就是 N1 问题。商品表关联了分类表1000 条商品就是 1000 次分类查询。解决在admin.py里重写get_queryset用select_related(category)预加载关联对象。同时给list_select_related [category]加上Django Admin 会自动做 join 查询。# goods/admin.py from django.contrib import admin from .models import Goods admin.register(Goods) class GoodsAdmin(admin.ModelAdmin): list_display (sku, name, category, price, status) list_select_related (category,) # 关键预加载外键 search_fields (sku, name) list_filter (status, category)5.4 时区设置导致时间差 8 小时现象入库单的created_at显示的时间比实际时间少 8 小时或者 Admin 后台时间不对。原因settings.py里USE_TZ True时Django 存的是 UTC 时间显示时按TIME_ZONE转换。如果TIME_ZONE设成了UTC中国用户看到的就是 UTC 时间。解决把TIME_ZONE改成Asia/ShanghaiUSE_TZ保持True。这样数据库存 UTC模板和 Admin 显示北京时间。不要为了省事把USE_TZ改成False那样跨时区部署会出问题。5.5 静态文件 404 导致 Admin 样式丢失现象DEBUG False部署后Admin 后台变成纯 HTMLCSS 和 JS 全部 404。原因Django 在DEBUG False时不再自动服务静态文件需要 Nginx 或 WhiteNoise 来处理。解决课程设计阶段最简单的办法是装whitenoise在MIDDLEWARE里加whitenoise.middleware.WhiteNoiseMiddleware然后python manage.py collectstatic收集静态文件。如果用了 Nginx就配置location /static/指向STATIC_ROOT目录。6. 进阶技巧用 Django ORM 的聚合查询做库存报表6.1 按月统计出入库总量的 ORM 写法毕业设计答辩时老师很喜欢问「你的系统有什么数据分析功能」。库存报表是最容易加分的。用 Django ORM 的annotate和TruncMonth可以一行代码搞定按月统计。# orders/services.py from django.db.models import Sum, Count from django.db.models.functions import TruncMonth from .models import StockOrder, StockOrderItem def monthly_report(year2024): 按月统计入库和出库的总数量 qs StockOrderItem.objects.filter( order__status1, order__created_at__yearyear ).annotate( monthTruncMonth(order__created_at) ).values(month, order__order_type).annotate( total_qtySum(quantity), order_countCount(order, distinctTrue) ).order_by(month) result {} for row in qs: month_str row[month].strftime(%Y-%m) type_name 入库 if row[order__order_type] 1 else 出库 result.setdefault(month_str, {})[type_name] { total_qty: row[total_qty], order_count: row[order_count], } return resultTruncMonth把时间截断到月values按月和单据类型分组Sum和Count做聚合。distinctTrue在Count里很重要因为一个单据有多个明细行不去重的话单据数会被重复计算。6.2 用 annotate 做商品出入库排名另一个实用报表是「出库量 Top 10 商品」。这个用annotate配合order_by就能实现def top_outbound_goods(limit10): 出库量排名前 N 的商品 return StockOrderItem.objects.filter( order__order_type2, order__status1 ).values( goods__name, goods__sku ).annotate( total_outSum(quantity) ).order_by(-total_out)[:limit]返回的 QuerySet 里每条包含商品名、SKU 和出库总量。前端拿到后直接用 ECharts 画柱状图就行。这个查询在数据量大时可能慢建议在StockOrderItem的order和goods字段上加联合索引。6.3 一个我踩过的坑聚合查询里的时区问题用TruncMonth做按月统计时如果USE_TZ True且TIME_ZONE Asia/ShanghaiDjango 会先把 UTC 时间转成上海时间再截断月份。这本身没问题但如果你的数据库是 MySQL 且没设时区TruncMonth可能按 UTC 截断导致月初几小时的单据被算到上个月。我的习惯是在DATABASES的OPTIONS里显式加TIME_ZONE: Asia/Shanghai并且在聚合查询前用timezone.localtime()确认时间转换正确。这个坑很隐蔽报表数字对不上时优先查这里。做这类系统我的习惯是先把数据模型和状态流转想清楚再写代码而不是上来就堆 CRUD。模型设计对了后面的视图和报表都是水到渠成的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表