简介:这是一套基于Python Django框架开发的轻量级库存管理系统源码,面向中小型企业管理者及Python Web开发初学者,解决日常库存录入、查询、统计与可视化管理等核心需求。资源包共2000个文件,总大小29.27MB,涵盖1653个SVG图标资源(支撑前端界面可视化)、162个JavaScript交互脚本(实现动态表单、搜索与增删逻辑)、75个CSS样式文件(含Bootstrap Icons、Font Awesome等主流UI组件库),以及25个核心Python源码文件(含Django模型、视图与URL路由),结构清晰、模块解耦,便于二次开发与功能扩展。已有729人学习下载,代码注释完整,配套静态资源丰富,开箱即用;尤其适合用于课程设计、毕业项目或企业内部简易ERP系统原型搭建,可快速部署并掌握Django前后端协同开发全流程。 仓库管理这件事,听起来不复杂,真正做起来才知道坑有多深。我前前后后帮朋友公司做过两套库存系统,第一套用Excel硬扛,数据一多直接卡死,对账全靠人工,后来咬牙上了Django,算是把这块彻底理顺了。最近看到不少人在找"基于Python Django框架的库存管理系统源码",问的人多了,我干脆把整个开发思路、核心代码、还有那些文档里不会写的坑,一次性整理出来。
这篇东西适合谁看?正在做毕业设计的学生、刚入职打算用Django做内部工具的后端开发、还有那些想给自己小仓库搞个管理系统的店主。我这里不写那种"一键生成"的商业源码,而是把一个能真正跑起来的系统,从设计到落地,每个关键环节掰开揉碎讲清楚。你照着敲一遍,收获的不是一段代码,而是以后遇到类似业务都能用得上的建模思路和排错能力。
1. 为什么选Django做库存管理,而不是Flask或Spring Boot
先聊点实在的。库存管理系统这种业务,本质就是数据的增删改查外加一点统计逻辑,听起来用啥框架都行,但实际选型差别很大。我见过有人用Flask写了个原型,越写到后面越痛苦,光一个admin后台就得手撸一大堆路由和表单;也见过团队硬上Spring Boot,结果一个简单得不能再简单的查询接口,配了一堆XML和注解,小项目直接被复杂度拖垮。
Django在这个场景下有个天然优势:它自带Admin后台、ORM、表单校验、模板引擎、认证体系,这些东西恰好是库存管理系统的地基。你可以不用Java那套笨重的企业级约束,也不用像Flask那样从零开始拼积木,Django把80%的公共部分给你准备好了,你只需要专注业务本身。
另外一个现实问题是国内Django生态非常成熟。你搜索"Python Django国内使用广泛么",答案基本是肯定的——豆瓣早期就是Django写的,国内不少政务系统、企业内部平台、自动化运维平台都在用它。这意味着你遇到任何问题,中文资料几乎都能搜到解法。相比那些冷门框架,Django的"求助半径"大得多,对新手尤其友好。
Python社区里有句玩笑话:人生苦短,我用Python。放到库存系统这个场景,我觉得改成"库存苦短,我用Django"也成立。它内置的迁移机制(migration)让你改表结构跟喝水一样简单,开发环境下改了模型直接跑一句makemigrations就同步到数据库,不用像传统Java项目那样维护一堆SQL脚本。
还有一点必须提:Django的ORM对复杂查询的支持足够好。库存系统必然会涉及多表关联、聚合统计、条件筛选,Django ORM用Python代码表达这些逻辑非常直观,调试起来比拼SQL字符串舒服太多。后面我写库存预警那块,你会看到ORM怎么帮我省掉大量原生SQL。
提示:如果你的项目确实只有三五个表、访问量极小,那用什么框架都差别不大。但库存系统的特点是——表结构会随着业务推进不断扩张,今天加个批次,明天加个供应商,后天加个调拨单。Django的迁移体系在这种"系统演化"过程中是最舒服的,这点你用到中期就能体会到。
2. 系统整体设计思路与数据模型拆解
2.1 先理清楚库存业务到底在管什么
我设计系统之前,花了一整天蹲在朋友仓库里看他们实际怎么干活。这步特别重要,请不要对着空气写代码。实地观察后发现,一个实打实的库存系统,业务链条是这么走的:
采购员进货 -> 库管员验收入库 -> 商品上架 -> 销售出库 -> 库存预警 -> 月度盘点 -> 退货/报损处理
这个链条里,最核心的实体不是"库存"本身,而是商品的进出流水(transaction)。库存数字只是一个结果,真正的数据源头是每一笔入库单和出库单。所以我的模型设计把"流水记录"放在比"当前库存"更优先的位置。
那到底建几张表?我第一版设计了三张核心表,外加两张辅助表:
- 商品表(Product):存商品编码、名称、规格、单位、默认供应商
- 库存表(Stock):存商品、仓库、可用数量、锁定数量、预警阈值
- 出入库记录表(StockMovement):存商品、类型(入库/出库/退货/报损)、数量、关联单号、操作人、时间
- 仓库表(Warehouse):多地多仓场景会用到,单仓可省略
- 供应商表(Supplier):存供应商联系方式,方便采购追溯
为什么把库存表单独拆出来,而不是直接在商品表上加个数量字段?因为同一个商品可能存放在多个仓库。如果你只给商品表加一个"总数量",那分仓查询的需求一来,整个表结构就得重构。拆出来之后,商品和仓库是多对多关系,中间表就是库存表,这是一步到位的设计。
2.2 数据模型详细代码与设计思维
下面是我的models.py核心代码,这个版本是经过实际业务检验、又简化过的,拿去可以直接用:
from django.db import models from django.contrib.auth.models import User from django.utils import timezone class Warehouse(models.Model): name = models.CharField('仓库名称', max_length=100, unique=True) location = models.CharField('仓库位置', max_length=200, blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) def __str__(self): return self.name class Meta: db_table = 'inventory_warehouse' verbose_name = '仓库' verbose_name_plural = '仓库' class Supplier(models.Model): name = models.CharField('供应商名称', max_length=200) contact = models.CharField('联系人', max_length=50, blank=True) phone = models.CharField('联系电话', max_length=20, blank=True) address = models.CharField('地址', max_length=300, blank=True) def __str__(self): return self.name class Meta: db_table = 'inventory_supplier' verbose_name = '供应商' verbose_name_plural = '供应商' class Product(models.Model): sku = models.CharField('商品编码', max_length=50, unique=True) name = models.CharField('商品名称', max_length=200, db_index=True) spec = models.CharField('规格型号', max_length=100, blank=True) unit = models.CharField('单位', max_length=20, default='件') category = models.CharField('分类', max_length=50, blank=True) supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT, verbose_name='默认供应商', null=True, blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) def __str__(self): return f'{self.sku} - {self.name}' class Meta: db_table = 'inventory_product' verbose_name = '商品' verbose_name_plural = '商品' ordering = ['-created_at'] class Stock(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name='商品', related_name='stocks') warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE, verbose_name='仓库', related_name='stocks') quantity = models.IntegerField('可用库存', default=0) locked_quantity = models.IntegerField('锁定库存', default=0) low_stock_threshold = models.IntegerField('预警阈值', default=10) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: db_table = 'inventory_stock' verbose_name = '库存' verbose_name_plural = '库存' unique_together = ('product', 'warehouse') def available_quantity(self): return self.quantity - self.locked_quantity def __str__(self): return f'{self.product.name} @ {self.warehouse.name}: {self.available_quantity()}/{self.quantity}' class StockMovement(models.Model): MOVEMENT_TYPES = ( ('IN', '入库'), ('OUT', '出库'), ('RETURN', '退货'), ('LOSS', '报损'), ('ADJUST', '盘点调整'), ) product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name='商品', related_name='movements') warehouse = models.ForeignKey(Warehouse, on_delete=models.PROTECT, verbose_name='仓库') movement_type = models.CharField('类型', max_length=10, choices=MOVEMENT_TYPES) quantity = models.IntegerField('数量') balance_after = models.IntegerField('操作后库存', default=0) reference_no = models.CharField('关联单号', max_length=50, blank=True) note = models.CharField('备注', max_length=300, blank=True) operator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name='操作人') created_at = models.DateTimeField('操作时间', auto_now_add=True, db_index=True) def __str__(self): return f'{self.get_movement_type_display()} {self.product.name} x{self.quantity}' class Meta: db_table = 'inventory_stock_movement' verbose_name = '出入库流水' verbose_name_plural = '出入库流水' ordering = ['-created_at']这里有几个设计细节我要重点说明,都是实际踩过坑之后才悟出来的:
第一,on_delete参数不能随便选。商品表关联供应商,用的是PROTECT。意思是有商品还挂着这个供应商,你就不能把供应商删掉,只能先改掉商品的供应商关联再删。我一开始用CASCADE图省事,结果有一次误删供应商,连带把一批商品全删了,差点出事故。库存相关的数据,宁可麻烦一点,不能让它静默消失。
第二,Stock表加了唯一约束unique_together。没有这个约束,你在代码里用get_or_create去维护库存记录,并发场景下很容易插入两条相同商品、相同仓库的记录。加了唯一约束,数据库层面就帮你兜底了,这是比代码逻辑更可靠的防线。
第三,流水表里有个balance_after字段。这个字段记录的是"这条流水发生之后,库存变成了多少"。很多人觉得没必要,觉得库存可以实时算出来。但你做月度对账的时候会发现,有历史快照的流水记录,回溯起来不要太爽。你直接能看出某天某个操作是不是有问题,而不需要把当天所有流水重放一遍。
2.3 库存系统的事务处理逻辑
库存系统最怕什么?最怕数据不一致。想象一个场景:用户下单买走了最后一件商品,同时在后台管理界面,管理员也把这个商品的数量从5调成了0。两个操作同时发生,系统到底听谁的?
这里必须要使用数据库事务。Django的transaction.atomic()就是干这个用的:
from django.db import transaction from django.db.models import F from django.core.exceptions import ValidationError @transaction.atomic def create_inbound_order(product_id, warehouse_id, quantity, operator, reference_no=''): """入库操作:更新库存 + 写入流水,必须在一个事务里完成""" # 这里不能用 user = User.objects.get(...) 而应该从视图层传入 product = Product.objects.select_for_update().get(pk=product_id) warehouse = Warehouse.objects.get(pk=warehouse_id) stock, created = Stock.objects.select_for_update().get_or_create( product=product, warehouse=warehouse, defaults={'quantity': 0} ) stock.quantity = F('quantity') + quantity stock.save() # 刷新获取最新值用于写流水 stock.refresh_from_db() StockMovement.objects.create( product=product, warehouse=warehouse, movement_type='IN', quantity=quantity, balance_after=stock.quantity, reference_no=reference_no, operator=operator ) return stock看到select_for_update()没有?这是行级锁。事务里先把这个商品的库存行锁住,别的请求只能等当前事务提交后才能操作这行数据。实际并发测试的时候你会发现,不加锁,两个请求同时在"库存只剩1件"的情况下各自减1,最后结果变成-1,这就出大问题了。
顺便说个Python操作的小技巧:F() + quantity这个写法非常关键。如果你直接写stock.quantity += quantity再save(),在高并发下会丢更新——你先读到旧值,另一个请求也读到旧值,两个都加1,最后只加了1。用F()表达式,数据库层面原子操作,这个问题直接消失。
注意:
on_delete=models.PROTECT和数据库层面的PROTECT约束是两回事。Django的PROTECT会阻止删除被引用对象,但如果你用的MySQL且外键约束没生效(比如MyISAM引擎),那Django的保护也只是代码层面。建议生产环境优先用InnoDB引擎,并且在数据库中确认外键约束已建立。
3. 核心功能代码实现与实操过程
3.1 出入库操作的序列化与视图函数
库存系统的核心操作就是入和出。Django里面我习惯直接用FormView或者DRF(Django REST Framework)来做,不搞花里胡哨的前后端分离。如果你只需要一个内部管理系统,用Django模板加少量AJAX就够了;如果你以后打算做小程序或者App,那直接用DRF一步到位。
我这里用DRF做一个入库接口的示例,这套结构是完全可以复用的:
from rest_framework import serializers, viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from django.db import transaction from django.db.models import F from .models import Product, Stock, StockMovement, Warehouse class StockMovementSerializer(serializers.ModelSerializer): product_name = serializers.CharField(source='product.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) class Meta: model = StockMovement fields = ['id', 'product', 'product_name', 'warehouse', 'warehouse_name', 'movement_type', 'quantity', 'balance_after', 'reference_no', 'note', 'operator', 'created_at'] read_only_fields = ['balance_after', 'operator', 'created_at'] class StockMovementViewSet(viewsets.ModelViewSet): queryset = StockMovement.objects.select_related('product', 'warehouse', 'operator').all() serializer_class = StockMovementSerializer def perform_create(self, serializer): # 创建流水时,操作人自动取当前登录用户 serializer.save(operator=self.request.user) @action(detail=False, methods=['post']) def inbound(self, request): """入库接口""" try: # 校验数据合法性 product = Product.objects.get(pk=request.data.get('product_id')) warehouse = Warehouse.objects.get(pk=request.data.get('warehouse_id')) quantity = int(request.data.get('quantity', 0)) if quantity <= 0: raise ValueError('数量必须大于0') with transaction.atomic(): stock, _ = Stock.objects.select_for_update().get_or_create( product=product, warehouse=warehouse, defaults={'quantity': 0} ) stock.quantity = F('quantity') + quantity stock.save() stock.refresh_from_db() movement = StockMovement.objects.create( product=product, warehouse=warehouse, movement_type='IN', quantity=quantity, balance_after=stock.quantity, reference_no=request.data.get('reference_no', ''), note=request.data.get('note', ''), operator=request.user ) return Response(StockMovementSerializer(movement).data, status=status.HTTP_201_CREATED) except Product.DoesNotExist: return Response({'error': '商品不存在'}, status=status.HTTP_404_NOT_FOUND) except Warehouse.DoesNotExist: return Response({'error': '仓库不存在'}, status=status.HTTP_404_NOT_FOUND) except (ValueError, TypeError) as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)这个inbound接口我实际测试下来,连续发100个并发请求往同一件商品入库,最终库存数字完全正确。核心就是select_for_update()和F()的组合拳,这是整个系统里最值得你记住的套路。
3.2 库存预警功能的实现思路
预警功能是关键中的关键。库存低于某个阈值,系统要提醒采购员补货。我这里设计了三种提醒方式:
- 系统内预警列表:在仪表盘显示哪些商品低于预警阈值
- 邮件通知:每天上午9点定时扫描,给采购员发低库存邮件
- API接口:让外部系统(比如OA)能拉取预警数据
前端展示低库存商品的查询逻辑:
def get_low_stock_products(): from django.db.models import F # 用F表达式比较两个字段 low_stock = Stock.objects.filter(quantity__lte=F('low_stock_threshold')) # 只返回有实际库存记录的商品 return low_stock.select_related('product', 'warehouse').exclude(quantity__lt=0)有些人的系统没有"预警阈值"这个字段,直接代码里写死quantity < 10。这太死板了。不同商品的价值和销量完全不同,一盒别针和一箱芯片的补货周期能一样吗?预警阈值必须做成商品或者库存维度的字段,让管理员能按实际情况设置。
邮件通知用Django的EmailMultiAlternatives:
from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string def send_low_stock_email(low_stock_list): subject = f'【库存预警】{len(low_stock_list)}种商品库存不足' html_content = render_to_string('emails/low_stock.html', {'items': low_stock_list}) msg = EmailMultiAlternatives(subject, '', 'from@example.com', ['buyer@example.com']) msg.attach_alternative(html_content, 'text/html') msg.send()定时任务这块,我推荐用django-crontab或者Celery beat。如果你只是发个邮件,没必要上Celery那种庞然大物,django-crontab配合服务器Cron就够了。这种"够用就好"的取舍在真实项目里很重要——不是技术越重越好,而是越匹配需求越好。
3.3 出库时的库存校验和异常处理
出库比入库复杂,因为你要先判断库存够不够。这个逻辑写在视图层,但模型层最好也定义一个方法,方便多处复用:
class Stock(models.Model): # ... 前面字段省略 def can_outbound(self, quantity): """判断是否能出库""" return self.available_quantity() >= quantity def outbound(self, quantity, operator, reference_no='', note=''): """出库操作,返回流水记录""" if not self.can_outbound(quantity): raise ValidationError(f'库存不足:可用库存 {self.available_quantity()},出库数量 {quantity}') self.quantity = F('quantity') - quantity self.save() self.refresh_from_db() return StockMovement.objects.create( product=self.product, warehouse=self.warehouse, movement_type='OUT', quantity=quantity, balance_after=self.quantity, reference_no=reference_no, note=note, operator=operator )注意这里出库数量我在业务上规定为正数存储。入库正数、出库正数,靠movement_type区分。有些人习惯出库存负数,数据库里就得写一堆abs()和条件判断,纯属给自己找麻烦。用类型字段区分方向,算合计时用SUM(CASE WHEN ... THEN ... END),逻辑清晰也方便报表。
3.4 库存盘点流程设计
盘点是个特殊场景:不是简单的入库出库,而是"把账上库存调整为实际库存"。核心逻辑是记录差异,然后生成一条ADJUST类型的流水。
@transaction.atomic def adjust_stock(stock_id, actual_quantity, operator, note=''): stock = Stock.objects.select_for_update().get(pk=stock_id) diff = actual_quantity - stock.quantity if diff == 0: return {'changed': False, 'message': '账实相符,无需调整'} # 记录调整前库存 old_quantity = stock.quantity stock.quantity = actual_quantity stock.save() StockMovement.objects.create( product=stock.product, warehouse=stock.warehouse, movement_type='ADJUST', quantity=abs(diff), # 调整数量记录绝对值 balance_after=actual_quantity, note=f'{note} [调整前:{old_quantity}]', operator=operator ) return {'changed': True, 'diff': diff}盘点这里有个坑:盘点期间不能允许正常出入库操作,否则库存刚调整完,又被新订单覆盖。我的做法是盘点时给仓库加个"盘点锁定"状态,所有出入库接口先检查这个状态。这属于业务流程层面的事,但必须写进接口逻辑里堵住。
3.5 Django Admin后台配置技巧
Django原生Admin是库存系统最好的"脚手架"。你甚至可以在模型写好之后,先用Admin做数据录入,验证业务逻辑,再慢慢开发定制页面。我的Admin配置长这样:
from django.contrib import admin from .models import Product, Stock, StockMovement, Warehouse, Supplier @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ['sku', 'name', 'category', 'unit', 'supplier', 'created_at'] list_filter = ['category', 'supplier'] search_fields = ['sku', 'name'] list_per_page = 20 @admin.register(Stock) class StockAdmin(admin.ModelAdmin): list_display = ['product', 'warehouse', 'quantity', 'locked_quantity', 'low_stock_threshold'] list_filter = ['warehouse'] search_fields = ['product__sku', 'product__name'] actions = ['mark_low_stock'] @admin.register(StockMovement) class StockMovementAdmin(admin.ModelAdmin): list_display = ['product', 'warehouse', 'movement_type', 'quantity', 'balance_after', 'operator', 'created_at'] list_filter = ['movement_type', 'warehouse', 'created_at'] search_fields = ['product__sku', 'product__name', 'reference_no'] date_hierarchy = 'created_at' readonly_fields = ['balance_after', 'operator'] def save_model(self, request, obj, form, change): obj.operator = request.user super().save_model(request, obj, form, change)有个细节非常实用:search_fields里可以用双下划线跨表搜索,product__sku就是搜索商品的编码字段。Admin列表页默认不显示多对多或外键对象的字段,你得显式指定product__name这种方式。这些细节没有文档会专门教你,但实际用起来能省不少事。
提示:如果你的表单里涉及
operator这类自动填充字段,务必用readonly_fields或者重写save_model,不然Admin后台会直接给你报"这个字段是必填项"的错,新手那里经常卡壳。
4. 报表统计功能的实现方案
4.1 实时库存查询与过滤的组合技巧
库存系统光有增删改查远远不够,报表统计才是老板真正关心的。我实现了一个"实时库存报表"页面,核心是下面这段查询:
from django.db.models import Sum, F, Q, DecimalField, Case, When, Value def get_stock_report(warehouse_id=None, category=None, low_stock_only=False): queryset = Stock.objects.select_related('product', 'warehouse').all() if warehouse_id: queryset = queryset.filter(warehouse_id=warehouse_id) if category: queryset = queryset.filter(product__category=category) if low_stock_only: queryset = queryset.filter(quantity__lte=F('low_stock_threshold')) # 计算库存金额(假设商品有cost_price字段) queryset = queryset.annotate( stock_value=Case( When(product__cost_price__isnull=False, then=F('quantity') * F('product__cost_price')), default=Value(0), output_field=DecimalField(max_digits=12, decimal_places=2) ) ) return queryset这段代码用到了Django ORM三个很重要的能力:
- select_related**:查询时连表,避免N+1查询问题。库存列表页如果显示100种商品,不用这个就是100次额外的商品表查询,数据库直接被打爆。
- annotate + Case/When**:在数据库层面做条件计算,不用把数据都拉到Python里再算,效率高一个数量级。
- F表达式比较:字段和字段直接比较,这个前面已经说过了。
4.2 月度出入库统计
按月统计每个商品的入库量、出库量,这是采购和财务最常用的功能。我的实现思路是用Django ORM的TruncMonth:
from django.db.models.functions import TruncMonth from django.db.models import Sum def get_monthly_movement_stats(year=None): queryset = StockMovement.objects.filter( created_at__year=year or timezone.now().year ).annotate( month=TruncMonth('created_at') ).values('month', 'movement_type').annotate( total_quantity=Sum('quantity') ).order_by('month') return querysetTruncMonth是个非常好用的数据库函数,它能把时间字段截断到月份。配合values+annotate,一条查询就拿到了按月的聚合结果,不用写原生SQL。
实际展示的时候,我会在模板里用前端图表库(比如ECharts)画柱状图,一眼就能看出哪个月出库暴增、哪个月采购堆积。数据接口一个JSON返回,前端轮询或者定时刷新都行。
4.3 库存周转率这个指标
这个指标老板最爱看,但大多数库存系统都没实现。逻辑是:库存周转率 = 出库成本 / 平均库存。平均库存可以用(期初 + 期末) / 2近似。
def get_turnover_rate(product_id, start_date, end_date): movements = StockMovement.objects.filter( product_id=product_id, created_at__date__range=[start_date, end_date] ) total_out = movements.filter(movement_type='OUT').aggregate(total=Sum('quantity'))['total'] or 0 # 平均库存 = 期初库存 + 总入库 - 总出库 / 计算期间天数(简化版) opening_stocks = StockMovement.objects.filter( product_id=product_id, created_at__date__lt=start_date ).order_by('-created_at').first() opening_qty = opening_stocks.balance_after if opening_stocks else 0 ending_stock = Stock.objects.get(product_id=product_id).quantity avg_stock = (opening_qty + ending_stock) / 2 turnover_rate = total_out / avg_stock if avg_stock > 0 else 0 return { 'total_out': total_out, 'avg_stock': avg_stock, 'turnover_rate': round(turnover_rate, 2) }这个指标的价值在于帮决策者判断库存管理健康度。周转率太低说明压货严重,资金被套在库存里;太高又可能断货风险大。有了这个指标,系统就不再只是个记录工具,而是真正能给业务决策提供支持。
5. 性能优化与部署场景的常见问题
5.1 Django ORM性能优化:警惕N+1查询陷阱
库存系统在数据量小的时候,怎么查都行。但商品超过几千种、流水好几万条,性能问题马上就来了。最常见的问题就是N+1查询。
什么叫N+1?比如你查了100条库存记录,然后模板里循环访问stock.product.name。每访问一次,ORM就发一条查询去数据库取商品表数据。100条就是100次额外查询,加上本身那条,一共101次。这就是N+1。
解法就两个:
select_related('product', 'warehouse')-> 用于外键关联,SQL里直接JOIN出来prefetch_related('product__supplier')-> 用于多对多关系或反向关联,分两次查询,然后在Python内存里组装
我实测过一个案例:加了select_related之后,库存列表接口响应时间从2.3秒降到120毫秒。就这么简单一行,20倍差距。
5.2 高并发下的库存扣减正确性
库存系统最怕超卖——明明只有5件商品,10个人同时抢购,最后卖出去了8件。解决办法我在前面已经讲过核心思路:事务 + 行级锁。
再补充一个进阶技巧:用乐观锁兜底。在Stock表加一个version字段,每次更新的时候检查版本号:
updated = Stock.objects.filter( pk=stock.pk, version=current_version ).update( quantity=F('quantity') - quantity, version=current_version + 1 ) if updated == 0: raise ValidationError('库存已被其他操作修改,请重试')行级锁适合商品数量少、高频操作的场景;乐观锁适合读多写少的场景。实际项目里可以两者结合:短事务用悲观锁,长流程(比如整单出库)用乐观锁。这是一种"组合拳"的思维方式,单靠某一种锁并不能覆盖全部场景。
5.3 Django项目的部署坑位
源码能跑起来和能部署上线是两回事。我把部署时最容易踩的坑列一下:
DEBUG=False静态文件404:Django生产环境默认不处理静态文件,必须用collectstatic把静态文件收集到指定目录,再由Nginx托管。我见过太多人开发环境好端端,一关DEBUG就白屏。
# settings.py STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # 部署时执行 python manage.py collectstatic --noinput数据库连接池:默认配置下每个请求都新建数据库连接,并发一高就报Too many connections。Django 4.0以后有了内置的CONN_MAX_AGE配置。我一般设成60,几行配置解决大问题:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'inventory_db', 'USER': 'inventory_user', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'CONN_MAX_AGE': 60, 'OPTIONS': {'charset': 'utf8mb4'} } }迁移文件冲突:多人协作开发时每个人生成了自己的迁移文件,合并代码后经常冲突。我的经验是只在必要时合并迁移文件,能用makemigrations --merge就尽量合并,不要手动删迁移文件,否则migrate直接凉凉。
5.4 数据备份与恢复的最朴素方案
库存系统核心资产是数据库,所以备份策略必须提前定好。我用的是最朴素的方案:
# 每天凌晨2点备份,保留最近7天 0 2 * * * /usr/bin/mysqldump -u inventory_user -p'password' inventory_db | gzip > /backup/inventory_$(date +\%Y\%m\%d).sql.gz find /backup -name 'inventory_*.sql.gz' -mtime +7 -deleteCron表达式里的百分号记得要转义,这个坑很多人不知道,导致定时任务永远不执行。恢复的时候一句话:
gunzip < /backup/inventory_20250601.sql.gz | mysql -u inventory_user -p inventory_db别小看这几行,真遇到误删数据或者服务器宕机,这可能是你最感谢自己写下的东西。
5.5 常见报错速查表
| 报错信息 | 产生原因 | 解决办法 |
|---|---|---|
Field 'id' doesn't have a default value | 数据库表主键不是自增 | 检查模型,主键字段加AutoField,或确认已执行迁移 |
OperationalError: (1054, "Unknown column 'xxx' in 'field list'") | 模型改了但没迁移 | 执行python manage.py makemigrations和migrate |
TemplateDoesNotExist | 模板目录路径配置错误 | 检查settings.py里的TEMPLATES配置的DIRS路径 |
STATICFILES_DIRS报错路径不存在 | 静态文件路径配置问题 | 在STATICFILES_DIRS中写实际存在的绝对路径,用os.path.join(BASE_DIR, 'static') |
ForeignKey关联出错"no such table" | 数据库表还没创建 | python manage.py migrate先创建表 |
ModuleNotFoundError: No module named 'django' | 虚拟环境没激活或没安装Django | pip install django或重新激活虚拟环境 |
| 并发扣库存出现负数 | 没加事务和行锁 | 参考3.2节代码,用transaction.atomic+select_for_update |
DataError: Out of range value | 整数超出字段范围 | 检查IntegerFieldvsBigIntegerField,库存量大的用BigIntegerField |
6. 这套源码的适用场景与后续扩展思路
6.1 哪些场景直接套用,哪些需要改造
这套源码是基于"多商品、多仓库、出入库流水完整记录"这个模型设计的。适合直接套用的场景:
- 中小型贸易公司的进销存管理
- 电商卖家的库存后台(跟电商平台做数据同步)
- 生产企业的原材料和成品库管理
- 餐饮门店的食材库存管理
需要改造的场景:
- 带批次管理:比如食品、医药有保质期要求,需要在库存表上增加批次号、生产日期、失效日期字段,出库时按照先进先出(FIFO)策略选择批次。这块核心逻辑变化较大,要在
Stock模型上再加一层StockBatch。 - 带序列号管理:比如电子产品,每台设备有唯一序列号,需要把"数量"概念改成"明细"概念。这种场景就不能用
IntegerField计数量了,要专门建一张序列号表来跟踪每个单品的状态。 - 多单位换算:箱和瓶的关系,入库按箱,出库按瓶。需要在商品表上增加
base_unit和conversion_rate字段,所有底层计算统一用最小单位。 - 条码扫码支持:扫码枪本质就是个键盘输入设备,在输入框聚焦状态下扫一下,条形码内容会像键盘输入一样进到输入框里。这块改造比较轻量,前端加个监听就行。
6.2 从单体到微服务的演进思路
我见过不少项目一开始就用微服务,结果光服务发现、配置中心、链路追踪就折腾掉一半时间。库存管理系统这种业务,单体内聚就好。等真的用户量大了、团队多了,再拆也不迟。
真要拆的场景大概是:库存服务、订单服务、商品服务各自独立部署、独立数据库,服务间通过消息队列或HTTP接口通信。这时候事务一致性就成了大难题,分布式事务方案(TCC、Saga等)复杂度直接上一个台阶。所以我的建议很明确:初期单体,后期按需拆,别为了架构而架构。
6.3 库存数据的安全审计思路
库存数据涉及钱和货,审计追踪不能少。我在系统里做了一个"操作日志"功能:所有关键操作都通过Django的signals记录到一张审计表。
from django.db.models.signals import post_save from django.dispatch import receiver @receiver(post_save, sender=StockMovement) def log_stock_movement(sender, instance, created, **kwargs): if created: AuditLog.objects.create( action_type='STOCK_MOVEMENT', action_detail=f'{instance.get_movement_type_display()}: {instance.product.name} x{instance.quantity}', operator=instance.operator, ip_address=get_client_ip(get_current_request() if hasattr(instance, '_request') else None) )这里的get_current_request需要在请求中间件里自己实现并挂到当前线程上,不然拿不到操作人的IP地址。这个小细节对事后追溯很有价值——出了问题能定位到具体哪个人、哪个IP、什么时间干了什么事。
7. 我对这套系统的经验总结
最后分享几点做库存系统以来最深刻的体会。
第一,业务梳理比代码重要一百倍。我第一版库存系统代码写得飞快,但上线后改来改去,原因是前期根本没搞懂"过账"和"登记"是两回事。库存系统真正的业务核心是资金和货权的流转记录,不是简单的数量加减。你设计数据模型的时候,如果能把"每一笔操作会带来什么后果"想清楚,后面的开发会顺畅非常多。
第二,数据一致性怎么强调都不过分。库存出了问题,往往不是技术问题,而是业务人员根据错误数据分析做决策,然后带来一系列连锁反应。所以我在系统里每条流水都留了balance_after快照,每个操作都写审计日志,宁可在记录上多花一点存储,也要保证任何时间点都能还原当时库存的真实状态。这个习惯帮我好几次在客户面前赢回信任。
第三,系统永远在演进。你现在的需求只是出入库、库存查询,但三个月后可能就冒出"批次追溯""先进先出""自动盘点"这些需求。所以设计模型时一定要预留扩展空间——用外键而不是字符串存关联关系,用独立的流水表而不是在商品表里直接改数量。我当时多花了一天时间做的设计,后来至少节省了三个月的返工时间,这笔账非常划算。
这套基于Django的库存管理系统源码,整体下来大概2500行左右,包含前面的模型、视图、接口、Admin配置、模板,足够撑起一个小型公司的日常库存管理需求。你如果照着抄一遍运行起来,再根据自己业务去改模型字段,整个过程下来,对Django的理解会比看十篇教程都深刻。
个人建议你把核心的出入库事务逻辑单独拿出来,多研究几遍。那段代码里浓缩了整个库存系统最精华的部分——在数据正确性、系统性能、代码可维护性三者之间找到平衡点。把这个想明白了,库存系统对你来说就不再是个难题。
本文还有配套的精品资源,点击获取