尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Django库存管理系统实战:从数据库设计到部署上线

Django库存管理系统实战:从数据库设计到部署上线 简介这是一套基于Python Django框架开发的轻量级库存管理系统源码面向中小型企业的IT人员、Python初学者及Web开发学习者旨在解决日常库存数据录入、查询、统计与基础业务流转等实际管理需求。资源包共2000个文件总大小29.27MB涵盖1653个SVG图标资源支撑界面可视化、162个JavaScript交互脚本实现动态表单与实时响应、75个CSS样式文件含Bootstrap Icons与Font Awesome等主流UI组件库以及25个核心Python源码文件含Django模型、视图与URL路由逻辑结构完整、开箱即用。已有729人下载学习适合用于课程设计、毕业项目或企业内部简易系统二次开发。读者可直接部署运行快速掌握Django前后端协同开发流程并通过大量现成UI资源理解企业级管理后台的样式组织与图标集成规范。 做库存管理系统这个需求这些年一直有人在找。网上一搜“Django 库存管理系统源码”出来的结果一大把但下载下来跑一跑就会发现多数是课设水平——能加商品、能入库、能出库一到真实业务场景就露馅库存对不上、权限全空白、单据没法追溯、多人同时用就卡死。我在帮朋友的仓储做这套系统之前也翻过不少开源项目最后决定按自己的方式从零搭一套能真正落地跑的版本。这套基于 Python Django 框架的库存管理系统核心难点并不在“增删改查”而是把库存业务里最容易出错的几个环节处理好数据一致性、单据流转、角色权限、审计留痕。这篇文章我就从项目选型、数据库建模、核心业务逻辑实现一直讲到部署上线的坑把完整思路和关键代码一步步拆开说适合正在做课程设计或毕业设计的朋友也适合中小团队想自己搭一套进销存系统的开发者。1. 为什么Django是这类业务系统的稳妥选择选型与前情提要1.1 业务系统的核心诉求恰好是Django的强项库存管理系统本质上是一个典型的管理信息系统它最大的特点是什么是“角色多、单据多、权限要求明确、数据安全要求高”。这种系统的开发诉求和 Django 的强项几乎是完全对齐的。用户认证与权限Django 自带的 User、Group、Permission 机制开箱即用不用自己造轮子。数据库迁移Django ORM 的 migration 机制能让你在开发过程中反复修改表结构而不用手写 SQL 脚本去同步。后台管理Django Admin 在某些场景下可以直接当内部运营后台用省下大量重复页面开发时间。安全防护CSRF 防护、SQL 注入防护、XSS 转义这些都是框架自带的对一个涉及库存金额的系统来说这一点比很多人想象的重要得多。很多人纠结 Django 和 Flask 选哪个。我的结论很直接如果项目是 API 服务、或者高度定制化的轻量接口Flask 没问题但如果是库存系统这种业务密集型的 Web 系统Django 的“全家桶”优势非常明显。你想想用 Flask 做同样一套系统要额外接 Flask-Login、Flask-Admin、Flask-Migrate、Flask-SQLAlchemy还要自己处理各组件之间的兼容性和配置方式。Django 把这些都集成好了而且约定统一团队协作时也不容易各写各的风格。1.2 功能边界要清醒做什么不做什么做项目的第一步其实不是写代码而是划边界。我见过太多人一上来就想做个“企业级 WMS”结果做了三个月还在做采购审批流。我的建议是第一版只做最小可用闭环把业务主流程打通再逐步加功能。这套系统的功能边界如下模块功能范围商品管理商品分类、SKU 编码、供应商信息、安全库存上下限入库管理采购入库、入库单管理、入库明细记录出库管理销售出库、出库单管理、库存不足校验盘点管理盘点单创建、实盘数量录入、差异调整流水库存预警低于安全库存自动提示支持邮件/站内通知系统管理用户管理、角色权限分配、操作日志第一版不做多仓库不做复杂的审批流不做加权平均成本核算。入库单和出库单保存即生效。这些不是“功能缺陷”而是刻意的边界控制——先把主线跑通后面扩展都很方便。技术栈方面我选择 Django 4.2 LTS PostgreSQL Bootstrap 5。开发环境用 SQLite 也完全没问题生产切换 PostgreSQL 就行。这套组合的特点是成熟、稳定、社区资料多遇到坑基本都能搜到解决方案。2. 数据库设计比写业务代码更重要实体梳理与表结构拆解2.1 库存数据建模的核心思想别把“当前库存”当唯一事实这里我要先讲一个很多人容易忽略的设计决策商品表里的 current_stock 字段到底该不该存在如果你去搜一些网上流传的“库存管理系统源码”会发现大多数是这么做的商品表里有一个 stock 字段入库就加出库就减完事了。看起来很直白但实际操作中会有几个致命问题无法追溯库存从 100 变成 80是销售出库了还是盘点盘亏了还是退货被扣了如果没有流水记录你将永远无法回答这个问题。并发扣减容易出错两个人同时下单都读到库存 5都判断“够扣”都执行减 1最后库存变成 3而不是预期的 4。这在 SQLite 这种数据库上尤其容易出现。出错后无法修复一旦 current_stock 的数值因为某个 bug 错了你没有任何依据来校准它。只能手动改数据库改完也不知道数据对不对。所以正确的设计思路是库存流水表StockTransaction才是唯一事实来源商品表的 current_stock 只是一个冗余缓存字段。每次业务操作事务性地写入一条库存流水并同步更新商品的缓存字段。这样即使 current_stock 哪天被改坏了也能通过流水表重算整个库存历史来修复。2.2 核心数据表设计我用 Django 的 Model 代码来展示这套设计。首先是基础资料部分供应商、商品分类、商品。# models.py 基础资料部分 from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(max_length64, uniqueTrue, verbose_name分类名称) class Meta: verbose_name 商品分类 verbose_name_plural 商品分类 def __str__(self): return self.name class Supplier(models.Model): name models.CharField(max_length128, verbose_name供应商名称) contact models.CharField(max_length32, blankTrue, verbose_name联系人) phone models.CharField(max_length32, blankTrue, verbose_name联系电话) address models.CharField(max_length255, blankTrue, verbose_name地址) class Meta: verbose_name 供应商 verbose_name_plural 供应商 def __str__(self): return self.name class Product(models.Model): sku models.CharField(max_length64, uniqueTrue, db_indexTrue, verbose_nameSKU编码) name models.CharField(max_length128, verbose_name商品名称) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name商品分类) supplier models.ForeignKey(Supplier, on_deletemodels.PROTECT, verbose_name供应商) unit models.CharField(max_length16, default件, verbose_name单位) purchase_price models.DecimalField(max_digits10, decimal_places2, verbose_name采购价) sale_price models.DecimalField(max_digits10, decimal_places2, verbose_name销售价) low_stock_threshold models.IntegerField(default0, verbose_name安全库存下限) current_stock models.IntegerField(default0, verbose_name当前库存缓存字段) is_active models.BooleanField(defaultTrue, verbose_name是否启用) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 商品 verbose_name_plural 商品 def __str__(self): return f{self.sku} - {self.name}这里有几个细节值得展开说on_deletemodels.PROTECT 是我刻意选的。很多人教程里喜欢用 CASCADE意思是父表删了子表跟着删。这在库存系统里是灾难。比如一个商品已经被历史入库单、出库单引用了你把商品删了带 CASCADE 的结果就是历史单据跟着一起消失整个账目链条就断了。PROTECT 则是“如果还有引用就不允许删除”这才是业务系统应该有的行为。至于 Category 和 Supplier 也是一样被商品引用时不允许删除。商品表里保留 current_stock 字段但它不是实时通过流水统计出来的。每一次业务操作我们都会在事务里维护这个缓存值。为什么要保留它因为查询列表页的时候直接从商品表读 current_stock 是非常快的。如果每次都要从流水表 SUM 一遍几百种商品可能还好几千上万种商品就明显变慢了。用空间换时间缓存字段是一种“查询优化”但它的更新必须和流水写入放在同一个事务里否则就会不一致。然后是单据类表入库单、入库明细、出库单、出库明细。class InboundOrder(models.Model): STATUS_CHOICES ( (pending, 待审核), (completed, 已完成), (cancelled, 已作废), ) order_no models.CharField(max_length32, uniqueTrue, verbose_name入库单号) supplier models.ForeignKey(Supplier, on_deletemodels.PROTECT, verbose_name供应商) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultpending, verbose_name状态) operator models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL, verbose_name操作人) remark models.TextField(blankTrue, verbose_name备注) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 入库单 verbose_name_plural 入库单 class InboundItem(models.Model): inbound_order models.ForeignKey(InboundOrder, related_nameitems, on_deletemodels.CASCADE, verbose_name入库单) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name商品) quantity models.IntegerField(verbose_name入库数量) unit_price models.DecimalField(max_digits10, decimal_places2, verbose_name入库单价) class Meta: verbose_name 入库明细 verbose_name_plural 入库明细 class OutboundOrder(models.Model): STATUS_CHOICES ( (pending, 待审核), (completed, 已完成), (cancelled, 已作废), ) order_no models.CharField(max_length32, uniqueTrue, verbose_name出库单号) customer_name models.CharField(max_length128, blankTrue, verbose_name客户/领用部门) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultpending, verbose_name状态) operator models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL, verbose_name操作人) remark models.TextField(blankTrue, verbose_name备注) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 出库单 verbose_name_plural 出库单 class OutboundItem(models.Model): outbound_order models.ForeignKey(OutboundOrder, related_nameitems, on_deletemodels.CASCADE, verbose_name出库单) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name商品) quantity models.IntegerField(verbose_name出库数量) unit_price models.DecimalField(max_digits10, decimal_places2, verbose_name出库单价) class Meta: verbose_name 出库明细 verbose_name_plural 出库明细这里父子表的 on_delete 是不一样的InboundItem 在外键到 InboundOrder 时用了 CASCADE因为明细离开单据没有任何独立存在的意义而 InboundItem 外键到 Product 用 PROTECT因为商品被明细引用后不允许删除。同一个外键设计不同位置不同策略这是理解 Django 数据完整性的关键。库存流水表和盘点表class StockTransaction(models.Model): TRANSACTION_TYPES ( (inbound, 采购入库), (outbound, 销售出库), (stocktake, 盘点调整), (return, 退货), ) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name商品) transaction_type models.CharField(max_length20, choicesTRANSACTION_TYPES, verbose_name业务类型) quantity_change models.IntegerField(verbose_name变动数量正入负出) quantity_after models.IntegerField(verbose_name变动后库存余量) reference_no models.CharField(max_length64, blankTrue, db_indexTrue, verbose_name关联单号) operator models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL, verbose_name操作人) remark models.TextField(blankTrue, verbose_name备注) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue, verbose_name创建时间) class Meta: verbose_name 库存流水 verbose_name_plural 库存流水 indexes [ models.Index(fields[product, -created_at], nameprod_created_idx), ] class Stocktake(models.Model): STATUS_CHOICES ( (draft, 盘点中), (completed, 已完成), ) stocktake_no models.CharField(max_length32, uniqueTrue, verbose_name盘点单号) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultdraft, verbose_name状态) operator models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL, verbose_name盘点人) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) completed_at models.DateTimeField(nullTrue, blankTrue, verbose_name完成时间) class Meta: verbose_name 盘点单 verbose_name_plural 盘点单 class StocktakeItem(models.Model): stocktake models.ForeignKey(Stocktake, related_nameitems, on_deletemodels.CASCADE, verbose_name盘点单) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name商品) book_quantity models.IntegerField(verbose_name账面库存) actual_quantity models.IntegerField(verbose_name实盘数量) difference models.IntegerField(default0, verbose_name差异数量) class Meta: verbose_name 盘点明细 verbose_name_plural 盘点明细这里流水表有个字段特别重要quantity_after也就是“本次变动后的库存余量”。为什么要存这个冗余字段因为当你想知道“某一天某件商品的库存是多少”时直接查那笔流水记录的值就行而不用再从头累加。这在盘点对账、财务审计时极其实用。索引设计上我给 created_at 和 reference_no 加了 db_index并给 product created_at 建了联合索引。外键字段 Django 会自动建索引所以不用手动加。这些索引在数据量上来之后能明显改善列表查询和流水查询的速度。3. 核心业务链路的实现细节入库、出库、盘点和预警3.1 入库业务把“买了东西”变成“可追查的数据变更”入库业务的核心逻辑不是简单地给商品 current_stock 加数量而是要保证“入库单 入库明细 库存流水 商品库存缓存”这四样东西是同一个事务里一起完成的。任何一个环节单独提交都可能在系统崩溃或并发操作时造成数据不一致。看代码# services/inbound_service.py from decimal import Decimal from django.db import transaction from django.db.models import F from django.utils import timezone from inventory.models import (Product, InboundOrder, InboundItem, StockTransaction) class InsufficientStockError(Exception): pass transaction.atomic def create_inbound_order(supplier, items, operator, remark): items: [{product_id: 1, quantity: 10, unit_price: Decimal(5.00)}, ...] order_no generate_order_no(IN) inbound_order InboundOrder.objects.create( order_noorder_no, supplier_idsupplier, operatoroperator, statuscompleted, remarkremark, ) for item in items: # 行锁锁定商品记录防止并发修改 product Product.objects.select_for_update().get(pkitem[product_id]) quantity int(item[quantity]) unit_price Decimal(item[unit_price]) # 创建入库明细 InboundItem.objects.create( inbound_orderinbound_order, productproduct, quantityquantity, unit_priceunit_price, ) # 更新商品库存缓存字段 product.current_stock F(current_stock) quantity product.save(update_fields[current_stock]) # 写入库存流水 product.refresh_from_db() StockTransaction.objects.create( productproduct, transaction_typeinbound, quantity_changequantity, quantity_afterproduct.current_stock, reference_noorder_no, operatoroperator, remarkremark, ) return inbound_order这里有几个关键点必须讲清楚为什么要用 select_for_update()这是数据库层面的行锁。两个请求同时给同一个商品入库时没有锁就会互相覆盖。加上行锁后第二个请求会等待第一个请求提交事务后才继续执行这样 inventory 的加减操作就安全了。为什么要用 F(current_stock) quantity而不是先查出来在 Python 里加了再保存F 表达式是数据库层面的原子操作。如果先 SELECT 读出来在 Python 内存里加 1再 UPDATE 回去这个“读-改-写”的过程在并发场景下本身就可能互相覆盖。F 表达式让数据库直接执行“当前值 1”没有中间状态更安全。为什么 update 之后要 refresh_from_db()因为用了 F 表达式后product.current_stock 在 Python 内存里很可能还是旧值。如果不重新从数据库刷新我们写入流水表的 quantity_after 就是一个错误的值。这个坑非常隐蔽我第一次写的时候就没注意导致流水表的“变动后余量”和商品表实际值对不上。3.2 出库业务库存不足校验与并发下的安全扣减出库业务的难点和入库不同它多了两个问题一是库存不足怎么办二是并发时如何避免超卖库存被减成负数。# services/outbound_service.py from decimal import Decimal from django.db import transaction from django.db.models import F from inventory.models import (Product, OutboundOrder, OutboundItem, StockTransaction) transaction.atomic def create_outbound_order(customer_name, items, operator, remark): items: [{product_id: 1, quantity: 3}, ...] order_no generate_order_no(OUT) outbound_order OutboundOrder.objects.create( order_noorder_no, customer_namecustomer_name, operatoroperator, statuscompleted, remarkremark, ) for item in items: product Product.objects.select_for_update().get(pkitem[product_id]) quantity int(item[quantity]) # 库存不足直接抛异常整个事务回滚 if product.current_stock quantity: raise InsufficientStockError( f商品 {product.sku} - {product.name} 库存不足当前库存为 {product.current_stock}需要 {quantity} ) product.current_stock F(current_stock) - quantity product.save(update_fields[current_stock]) product.refresh_from_db() StockTransaction.objects.create( productproduct, transaction_typeoutbound, quantity_change-quantity, quantity_afterproduct.current_stock, reference_noorder_no, operatoroperator, remarkremark, ) OutboundItem.objects.create( outbound_orderoutbound_order, productproduct, quantityquantity, unit_priceproduct.sale_price, ) return outbound_order这里的核心是“先锁后查再扣”。行锁保证了严格的并发控制。关于库存不足校验我第一次做的时候直接把异常抛给前端显示后来发现用户体验很差——一个出库单里有 5 种商品第 3 种库存不足前面的商品全都被回滚了用户得重新填一整个表单。后来改成在表单提交之前先做一次预检把所有库存不足的商品一次性列出来用户改完再提交。这样虽然预检本身不锁行但可以提前拦截大部分错误。另外关于“出库单明细里的 unit_price 应该是什么”不同系统做法不同。有的从商品表带出当前售价好一点的会用“成本均价”作为出库单价。我的建议是第一版直接用商品表的 sale_price并且把出库单价快照到明细里保证历史单据上的价格不会随着商品价格调整而变化。这是一开始就要考虑的问题否则以后对账会很难受。3.3 盘点业务把账面库存校准为实际库存盘点是一个容易被轻视、但在仓库管理里非常核心的功能。它的本质是账面库存是系统算的实际库存是仓库里真实存在的。两者不一致时要以实际库存为准调整系统数据并把差异记成流水。盘点流程通常分三步创建盘点单记录当时的账面库存→ 录入实盘数量 → 审核确认系统自动生成调整流水。# services/stocktake_service.py from django.db import transaction from inventory.models import Stocktake, StocktakeItem, StockTransaction, Product transaction.atomic def create_stocktake(operator): 创建盘点单并生成所有商品的账面库存快照 stocktake Stocktake.objects.create( stocktake_nogenerate_order_no(ST), operatoroperator, statusdraft, ) products Product.objects.filter(is_activeTrue) for product in products: StocktakeItem.objects.create( stocktakestocktake, productproduct, book_quantityproduct.current_stock, actual_quantityproduct.current_stock, difference0, ) return stocktake transaction.atomic def complete_stocktake(stocktake_id, operator): 完成盘点按实盘数量调整库存 stocktake Stocktake.objects.select_for_update().get(pkstocktake_id) if stocktake.status completed: raise ValueError(该盘点单已完成不能重复操作) items stocktake.items.select_related(product) for item in items: product Product.objects.select_for_update().get(pkitem.product_id) diff item.actual_quantity - product.current_stock if diff 0: continue # 更新商品库存 product.current_stock item.actual_quantity product.save(update_fields[current_stock]) # 写盘点调整流水 StockTransaction.objects.create( productproduct, transaction_typestocktake, quantity_changediff, quantity_afteritem.actual_quantity, reference_nostocktake.stocktake_no, operatoroperator, remarkf盘点调整实盘{item.actual_quantity}账面{item.book_quantity}, ) # 更新盘点明细里的差异数量 item.difference diff item.save(update_fields[difference]) stocktake.status completed stocktake.completed_at timezone.now() stocktake.save(update_fields[status, completed_at]) return stocktake这里有一个值得注意的细节盘点期间要不要锁定商品的出入库我的做法是“不锁定但记录差异时要对比当前实时库存而不是盘点单里的账面库存”。为什么因为你创建盘点单时生成了账面库存快照然后仓库人员花了两小时盘完期间系统可能又发生了出库。如果用“实盘数量 - 账面快照”来算差异就会把盘点期间发生的正常出入库也当成盘盈盘亏数据就乱了。我的方案是完成盘点时用“当前实时库存”和“实盘数量”做比较盘点期间的出入库流水仍在正常记录这样差异只反映“盘点当年账面和实物不一致”的部分。这个细节虽然小但决定了盘点结果是否可信。3.4 库存预警不是库存为 0 才提醒而是低于安全线就报警预警功能看起来不复杂就是遍历商品检查 current_stock 是否低于 low_stock_threshold。但这里有个业务层面的问题阈值怎么定才合理我的经验是安全库存阈值不应该拍脑袋填。一个简单的参考公式是安全库存 平均日销量 × 采购周期天数 × 1.5安全系数。比如某商品平均一天卖 10 件供应商补货要 5 天那安全库存就设为 75 左右。这样你在上次采购用完、下次采购还没到货的窗口期库存仍然有缓冲。这个公式用哪种数据源去算取决于你有没有历史销售数据。没有就直接由管理员在商品维护里设置有的话可以在报表页计算后提示建议值。预警的触发方式第一版建议做成“站内通知 邮件”两种结合。站内通知就是系统里做一个未读消息列表登录后能看到邮件用 Django 的 send_mail 就行配合 Celery 定时任务每个小时扫一次或者每天早上发一次汇总不要频繁发不然没人看。# services/warning_service.py from django.db.models import F from django.core.mail import send_mail def get_low_stock_products(): return Product.objects.filter( is_activeTrue, current_stock__lteF(low_stock_threshold), ).order_by(current_stock) def send_low_stock_email(): products get_low_stock_products() if not products: return lines [f{p.sku} {p.name}当前库存 {p.current_stock}安全库存 {p.low_stock_threshold} for p in products] send_mail( subject库存预警通知, message\n.join(lines), from_emailNone, recipient_list[managerexample.com], )这个函数本身很简单但有个隐藏点用 F(low_stock_threshold) 做比较而不是先查出所有商品再在 Python 里过滤。原因是数据量大的时候数据库层面过滤效率高得多。4. 多角色权限与操作留痕让系统敢在企业里用4.1 角色权限Django 自带的权限系统够用吗很多“课设级别”的库存系统根本没有权限的概念任何打开页面的人都能删单据。这在真实场景里是绝对不行的。Django 自带的 Group Permission 机制对大多数内部管理系统来说完全够用不需要引入第三方库。Django 会自动为每个 Model 生成四个权限add、change、delete、view。我们可以创建两个 Group仓管员可以新增和查看入库单、出库单、盘点单不能删除单据不能管理用户。管理员所有权限包括用户管理和供应商管理。只读账号可选只分配 view 权限给老板或财务查看库存和报表用。在视图里控制权限我推荐用 Django 的类视图配合 PermissionRequiredMixin# views.py 权限控制示例 from django.contrib.auth.mixins import PermissionRequiredMixin from django.views.generic import ListView, CreateView from inventory.models import InboundOrder from inventory.forms import InboundOrderForm class InboundOrderListView(PermissionRequiredMixin, ListView): model InboundOrder template_name inventory/inbound_order_list.html permission_required inventory.view_inboundorder paginate_by 20 class InboundOrderCreateView(PermissionRequiredMixin, CreateView): model InboundOrder form_class InboundOrderForm template_name inventory/inbound_order_form.html permission_required inventory.add_inboundorder success_url /inventory/inbound-orders/如果你的视图不是类视图而是函数视图那就用 permission_required 装饰器。注意 view 权限在 Django 2.1 之后才自动生成如果你用的是很老的版本需要手动配。权限控制的另一个细节是“数据范围”仓管员能不能看到所有单据还是只能看到自己创建的如果不考虑数据隔离用 Django Permission 就够了。如果需要“我只能看自己创建的单据”那要在查询时加 filter(operatorrequest.user)。一般小团队的库存系统不需要这么严格的隔离按角色控制页面访问权限即可。4.2 操作日志Admin 的 LogEntry 是没用的Django Admin 自带 LogEntry 能记录后台增删改操作但你自己的业务页面比如新建入库单并不会自动写进 LogEntry。而且 LogEntry 的设计比较粗糙不符合业务审计的需求。我的做法是自定义一张操作日志表在业务 service 层统一记录。比如入库单创建成功就写一条日志“张三 创建了入库单 IN20250101001包含3种商品共120件”。这样一旦后续发现数据异常可以非常快速地定位是哪笔操作造成的。# models.py 操作日志 from django.db import models from django.contrib.auth.models import User class OperationLog(models.Model): user models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL, verbose_name操作人) action models.CharField(max_length64, verbose_name操作) model_name models.CharField(max_length64, verbose_name操作对象) object_id models.PositiveIntegerField(nullTrue, verbose_name对象ID) detail models.JSONField(defaultdict, verbose_name操作详情) created_at models.DateTimeField(auto_now_addTrue, verbose_name操作时间) class Meta: verbose_name 操作日志 verbose_name_plural 操作日志 ordering [-created_at]在入库 service 里加上日志记录def write_log(user, action, model_name, object_id, detail): if not user or not user.is_authenticated: return OperationLog.objects.create( useruser, actionaction, model_namemodel_name, object_idobject_id, detaildetail, )关键原则是日志要在事务内写入不要捞在外面。如果事务后来回滚了日志也要跟着回滚才对否则会出现“日志说创建成功了但单据不存在”的诡异情况。4.3 数据安全的基础配置权限之外还有几个最基础的安全配置很多新手项目会漏掉关闭 DEBUG生产环境 settings.py 里 DEBUG 必须设置为 False否则报错页面会直接把服务器代码路径和配置信息暴露给用户。SECRET_KEY 不要硬编码放到环境变量里避免提交到 Git 仓库后泄漏。这个泄露可能被拿去伪造签名、重置会话后果很严重。开启 CSRF 防护Django 默认开启但如果你写了 AJAX POST 请求记得把 CSRF Token 放进请求头否则会一直 403。数据库访问控制不要把数据库密码写死在 settings.py 里用环境变量或者 .env 文件管理。这些看起来都是老生常谈但我见过不止一个项目把这些基本项做错。别嫌基础基础的坑最致命。5. 页面层的快速落地表单、列表和库存看板的实践5.1 前端选型模板 Bootstrap 还是前后端分离关于库存管理系统这种内部系统我的建议很明确用 Django 模板 Bootstrap不要一上来就搞 Vue/React 前后端分离。原因很简单内部管理系统的核心诉求是“快点上线、方便维护、所有数据都能被搜索引擎和浏览器正常处理”而不是“构建复杂的用户交互体验”。模板 Bootstrap 的写法服务端渲染页面刷新就能完成所有操作逻辑简单直接。前后端分离意味着你至少要维护两套代码还要处理接口鉴权、跨域、CSRF Token 下发等一堆事情对一个业务系统来说纯属增加复杂度。当然如果你的项目已经到了“需要大量异步交互、实时库存看板、多端复用”的阶段再考虑前后端分离也不迟。但第一版老老实实用模板。5.2 列表页的性能优化select_related 和分页必须做商品列表页是库存系统里最常被访问的页面之一也是最容易出现 N1 查询问题的地方。什么是 N1就是你查询了 N 条商品记录每条商品都要再查一次它的分类和供应商结果就是 1 次主查询 N 次关联查询。商品一多页面响应就会肉眼可见地变慢。解决办法是 select_related。它通过 SQL 的 JOIN 直接把关联表的数据一次性查出来把 N1 变成 1 次查询。# views.py 商品列表 from django.views.generic import ListView from django.db.models import Q from inventory.models import Product class ProductListView(ListView): model Product template_name inventory/product_list.html context_object_name products paginate_by 20 def get_queryset(self): queryset Product.objects.select_related(category, supplier).filter(is_activeTrue) keyword self.request.GET.get(keyword, ).strip() if keyword: queryset queryset.filter( Q(name__icontainskeyword) | Q(sku__icontainskeyword) ) return queryset.order_by(sku)paginate_by20 是必须的。如果系统里只有 50 件商品不分页问题不大但真实仓库的商品数量是几百上千一次性渲染全部 DOM 节点会非常卡。分页之后配合 Bootstrap 的翻页样式用户体验好很多。需要注意的是 Q 对象里 icontains 在 PostgreSQL 上做的是不区分大小写的子串匹配搜 SKU 和搜商品名都够用。如果商品数量到了几万级以上就要考虑引入全文搜索或数据库的 trigram 索引但那是后话。5.3 入库单表单动态增删明细行的实现入库单的表单比普通表单复杂因为要支持一次录入多种商品。最简单可控的方式是 Django Formset它的管理字段management_form会自动处理行数的变化。关键代码大概是这样的# forms.py from django import forms from django.forms import formset_factory from inventory.models import InboundItem class InboundItemForm(forms.Form): product forms.ModelChoiceField(querysetProduct.objects.filter(is_activeTrue), label商品) quantity forms.IntegerField(min_value1, label数量) unit_price forms.DecimalField(max_digits10, decimal_places2, label单价) InboundItemFormSet formset_factory(InboundItemForm, extra1, can_deleteTrue)在模板里用 JavaScript 动态增加行时只需要复制 empty_form 的空行模板更新其中的 form index 以及 management_form 的 TOTAL_FORMS 值即可。Bootstrap 5 官方的表单样式可以直接套用做出来的效果比大多数人想象的要整洁。我不推荐自己写 JS 拼 JSON 提交到后端然后手工解析不是因为不行而是因为你得自己处理数据校验、错误提示、字段类型转换这些琐事。Formset 把这些都替你兜底了。5.4 库存看板聚合查询和低库存提醒看板页是给管理者看的信息要少而精。我建议首页放四个模块顶部几个数据卡片商品总数、库存预警数、今日入库单数、今日出库单数、低库存商品 Top 10 列表、最近 10 笔出入库流水、最近 5 单盘点记录。数据库统计用 ORM 的聚合函数就行不用写原始 SQL# views.py 首页看板 from django.db.models import Count, Sum from django.views.generic import TemplateView from inventory.models import Product, InboundOrder, OutboundOrder, StockTransaction class DashboardView(TemplateView): template_name inventory/dashboard.html def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) context[total_products] Product.objects.filter(is_activeTrue).count p a hrefhttps://download.csdn.net/download/weixin_44087733/89032096 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表