尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Django的仓库管理系统:模型设计、事务与部署实践

基于Django的仓库管理系统:模型设计、事务与部署实践 简介基于Django框架打造的仓库与库存管理系统源码面向计算机专业课程设计和期末大作业场景是一份可直接运行的高分参考项目。压缩包共148个文件包含57个Python源码、46个编译缓存、15个HTML模板并配有CSS、JS、XML等前端与配置资源其中Python脚本实现核心业务逻辑HTML/CSS/JS搭建页面展示SQLite数据库和SQL文件支撑数据存储同时集成了Bootstrap样式整体体积仅579KB。目前已有1454人学习下载项目注释清晰初学者可借此理解Django的MVT架构、ORM操作和模板渲染有能力的开发者也能在现有代码上进行二次开发扩展仓库或库存管理功能。尤其内置了数据库文件解压后无需额外配置即可运行能快速看到前后端联调效果为课程设计验收或大作业答辩提供完整演示基础。1. 基于Django的仓库管理系统为什么值得自己搭一套大多数团队的库存管理最开始都在Excel里完成数据量超过千行后并发更新和版本冲突会让表格彻底失控。转为自建一套Python仓库管理系统源码库存管理系统源码基于Django核心目的不是省软件采购费而是拿回数据模型的主动权哪些字段需要冗余、哪些单据必须走事务、预警阈值放在哪一层这些决策只有在自己的代码里才能完全掌控。这套源码对两类人最有用要选型内部WMS的团队可以直接拿它做二次开发想理解Django事务、查询优化和部署流程的后端工程师可以把它作为完整范本。标题里的内含数据库文件意味着不需要从建表开始导入预置数据后即可快速启动。下文会从模型设计、数据库文件导入、核心业务事务再到生产部署把这条从代码到上线的完整路径走一遍让新手有操作路径让熟手能看出代码边界和可改造点。2. 仓库管理系统的核心Django模型设计与数据表关系2.1 库存模型需要哪些字段为什么不能只用一张表小项目常犯的一个错误就是把所有字段塞进一张表商品名、入库时间、仓库位置、剩余数量堆在一起查询时越来越慢。库存系统的核心是仓库、商品、批次、流水这几个实体之间的关系。先说字段选型仓库表需要名称、位置、负责人商品表需要SKU、条码、规格、单位库存表需要仓库外键、商品外键、当前数量、上限、下限流水表需要商品、仓库、变更类型、变更数量、操作前数量、操作后数量、操作人、备注。这里有个容易被忽略的点库存表存储的是当前快照流水表存储的是每一次变更两者缺一不可。快照保证查询速度流水保证可追溯。若只有快照盘点出错时无法追查原因若只有流水每次查库存都需要 sum 一遍数据量大时性能不可忍受。Django 把这种关系表达得很直接按最常见的实现方式先定义仓库实体from django.db import models from django.contrib.auth.models import User class Warehouse(models.Model): 仓库存放商品的物理或逻辑位置 name models.CharField(max_length64, verbose_name仓库名称) address models.CharField(max_length200, blankTrue, verbose_name地址) manager models.ForeignKey( User, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namewarehouses, verbose_name仓库管理员 ) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table warehouse verbose_name 仓库 def __str__(self): return self.name这段代码定义仓库实体的基础字段。on_deletemodels.SET_NULL表示管理员被删除时仓库仍然保留只是 manager 字段置空related_namewarehouses提供了反向查询路径user.warehouses能拿到该管理员名下的仓库列表。实际项目中若有多租户需求还需要在这里加owner字段做数据隔离。接下来是商品和库存快照表class Product(models.Model): 商品SKU 作为业务唯一键 sku models.CharField(max_length32, uniqueTrue, verbose_nameSKU) name models.CharField(max_length128, verbose_name商品名称) spec models.CharField(max_length128, blankTrue, verbose_name规格) unit models.CharField(max_length16, default件, verbose_name计量单位) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table product verbose_name 商品 class Inventory(models.Model): 库存快照商品仓库 联合唯一 product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameinventories) warehouse models.ForeignKey(Warehouse, on_deletemodels.CASCADE, related_nameinventories) quantity models.PositiveIntegerField(default0, verbose_name当前数量) min_quantity models.PositiveIntegerField(default0, verbose_name库存下限) max_quantity models.PositiveIntegerField(default0, verbose_name库存上限) updated_at models.DateTimeField(auto_nowTrue) class Meta: db_table inventory constraints [ models.UniqueConstraint(fields[product, warehouse], nameuniq_product_warehouse) ]这里最关键的是UniqueConstraint同一商品在同一仓库只能有一条库存记录这是防数据错乱的第一道保险。on_deletemodels.CASCADE表示商品或仓库删除时对应库存记录一并删除如果业务上不允许物理删除应改用is_active软删除字段而不是在代码里反复判断filter(is_activeTrue)。2.2 流水表的设计每次变更都追加为不可变记录库存变动的每个操作都应该留下一条流水记录。这个表会无限增长所以字段设计直接影响后续查询和归档策略。核心思路是流水表只增不改不删查询用索引覆盖归档用时间分区。class StockTransaction(models.Model): 库存流水每次数量变更追加一条记录 TYPE_CHOICES [ (in, 入库), (out, 出库), (adjust, 盘点调整), ] product models.ForeignKey(Product, on_deletemodels.PROTECT, related_nametransactions) warehouse models.ForeignKey(Warehouse, on_deletemodels.PROTECT, related_nametransactions) transaction_type models.CharField(max_length16, choicesTYPE_CHOICES, verbose_name业务类型) quantity models.PositiveIntegerField(verbose_name本次变更数量) before_quantity models.PositiveIntegerField(verbose_name变更前数量) after_quantity models.PositiveIntegerField(verbose_name变更后数量) operator models.ForeignKey(User, on_deletemodels.PROTECT, verbose_name操作人) remark models.CharField(max_length255, blankTrue, verbose_name备注) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table stock_transaction ordering [-created_at] indexes [ models.Index(fields[product, warehouse, -created_at]), ]流水表中的before_quantity和after_quantity是冗余字段但强烈建议保留。原因很简单任何环节出错时靠这两列就能回溯某个 SKU 在某仓库的连续变化曲线而不用依赖程序日志。on_deletemodels.PROTECT用来阻止删除商品或仓库时连带删除历史流水这是可追溯性的底线。把这几张表的关键设计点汇总如下表名关键字段约束 / 说明warehousename, address, manager_idmanager 可空SET_NULLproductsku, name, spec, unitsku 唯一索引inventoryproduct_id, warehouse_id, quantityproductwarehouse 联合唯一stock_transactiontype, quantity, before_quantity, after_quantity只追加不删不改表结构到位后下一步是把源码真正跑起来导入标题里说的数据库文件。3. 从源码到运行环境配置、依赖安装与数据库文件导入3.1 创建虚拟环境并安装 Django 依赖拿到源码后的第一个动作不是直接运行而是把 Python 环境隔离出来。这一步看起来基础但大多数我明明装了 Django 为什么还报错的问题都出在全局环境里的版本冲突。标题里内含数据库文件的仓库通常配套一个 requirements.txt 和一份 .sql 或 SQLite 文件下面按最常见的开工流程来走。cd warehouse_project python -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt在 Windows 下第二条命令要改成venv\Scripts\activate。虚拟环境建好后pip install -r requirements.txt会按固定版本号安装 Django 和第三方依赖避免能跑但不知道跑在什么版本上的隐性风险。装完依赖先确认 Django 版本是否与源码匹配python -m django --version python manage.py --help如果版本不匹配优先改虚拟环境里的 Django 版本不要直接改源码。比较常见的情况是源码基于 Django 3.2 开发而环境默认装了 Django 5.x两者在render_to_string、default_app_config等 API 上不兼容运行时会直接抛 AttributeError。另外在国产化服务器如麒麟、统信 UOS上部署时优先用系统 apt 源里的 Python 3.8 已验证过的 Django 版本避免源码编译带来额外麻烦。3.2 数据库配置从 SQLite 换到 MySQL 的完整改法很多开源 Django 仓库默认使用 SQLite因为它零配置。但仓库系统一旦要上生产必须迁移到 MySQL 或 PostgreSQL。这里以 MySQL 为例因为企业内网用它做业务库是最常见的做法。第一步创建数据库并授权CREATE DATABASE warehouse DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER warehouse_userlocalhost IDENTIFIED BY your_password; GRANT ALL PRIVILEGES ON warehouse.* TO warehouse_userlocalhost; FLUSH PRIVILEGES;第二步在 Django 的settings.py里替换DATABASES配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: warehouse, USER: warehouse_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 60, OPTIONS: { charset: utf8mb4, }, } }重点说两个参数。CONN_MAX_AGE设为 60 表示数据库连接复用 60 秒避免每次请求都重新握手能明显降低高并发下的连接开销charset必须设成utf8mb4否则商品名称里一出现 emoji 或生僻字就会报 Incorrect string value 错误。还有个容易忽略的细节OPTIONS里不要加init_command因为新版 PyMySQL 已经默认设置 utf8mb4再加反而会告警。3.3 数据库文件导入先迁移再灌数据源码内含数据库文件通常有两种形态一种是 SQLite 的.db文件另一种是 MySQL 的.sql导出文件。处理方式完全不同分开说。如果是 SQLite 文件直接把它复制到项目根目录并确认settings.py里的DATABASES路径指向正确DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / data.sqlite3, } }如果是.sql文件导入到 MySQL 的命令是mysql -u warehouse_user -p warehouse warehouse_dump.sql python manage.py migrate --fake-initialmigrate --fake-initial的含义是如果数据库里已经有 Django 的django_migrations历史记录只标记为已执行不实际创建表。这个参数在导入现成数据库文件后必须使用否则 Django 会尝试建同名表直接报错。注意--fake-initial只在目标库中已存在全部业务表的场景下使用。如果导入的是半份数据库文件部分表缺失fake 会跳过迁移逻辑后续代码引用缺失字段时会直接报 500。稳妥做法是导入前用SHOW TABLES;检查表数量再决定是否带这个参数。导入完成后快速验证数据可用性from warehouse_app.models import Product, Inventory print(Product.objects.count(), Inventory.objects.count())能正常输出统计数说明数据库文件已经接上。如果 count 为 0先去确认导入时有没有被历史数据覆盖再看django_migrations表是否完整。3.4 Django 管理后台的账号处理仓库系统的存量数据里通常只有库存和商品操作账号不包含在内。这时需要在 Django shell 里创建超级用户python manage.py createsuperuser别把内含数据库文件理解成账号也内置了。正常源码包最多预置几个测试账号口令首次启动后必须改密。管理后台是 Django 项目最容易被低估的模块它直接关联到 Model 定义仓库、商品、流水这几张表注册进 admin.py 后日常的数据修正和盘点核对都能在后台完成不需要写脚本。4. 仓库业务的核心操作入库、出库与预警的 Django 事务实现4.1 入库操作为什么必须用transaction.atomic()包裹入库操作看上去简单找到库存表把数量加进去再写一条流水。但业务场景中同一个人可能同时从多个窗口入库两个请求同时读到旧值然后各自写回造成数量丢失。Django 里解决这个问题的标准方案是select_for_update()配合transaction.atomic()前者生成SELECT ... FOR UPDATE语句把记录锁住后者保证所有写操作在同一事务内提交或回滚。from django.db import transaction from .models import Product, Inventory, StockTransaction transaction.atomic def stock_in(product_id, warehouse_id, quantity, operator): 入库锁定商品 - 更新库存快照 - 追加流水 # 对商品行加锁防止并发创建重复库存记录 product Product.objects.select_for_update().get(pkproduct_id) inventory, created Inventory.objects.get_or_create( productproduct, warehouse_idwarehouse_id, defaults{quantity: 0, min_quantity: 0, max_quantity: 0} ) if not created: inventory.quantity quantity inventory.save() StockTransaction.objects.create( productproduct, warehouse_idwarehouse_id, transaction_typein, quantityquantity, before_quantityinventory.quantity - quantity, after_quantityinventory.quantity, operatoroperator, ) return inventory逻辑说明先锁定商品记录因为get_or_create在并发时存在竞态如果不锁定两个线程可能同时创建两条库存记录。然后更新或创建库存快照再追加流水。整个函数被transaction.atomic包裹任何一步抛异常都会整体回滚数量不会出现半更新状态。参数上quantity要单独做校验比如传 0 或负数必须在前置逻辑里拦截。operator不是简单的用户名字符串而应该传User实例这样流水的operator_id才能关联到真实账号后续审计才有据可查。4.2 出库操作库存校验与防超卖出库比入库多一个前置动作检查当前数量够不够。防超卖的关键在于校验和扣减必须在同一个事务内完成否则两个请求同时通过校验就会超卖。下面的实现是电商场景里常见的做法transaction.atomic def stock_out(product_id, warehouse_id, quantity, operator, remark): 出库锁定库存行 - 校验数量 - 扣减 - 追加流水 # 行级锁锁定库存记录防止两个请求同时扣减 inventory Inventory.objects.select_for_update().get( product_idproduct_id, warehouse_idwarehouse_id ) if inventory.quantity quantity: raise ValueError(f库存不足当前库存 {inventory.quantity}) inventory.quantity - quantity inventory.save() StockTransaction.objects.create( productinventory.product, warehouseinventory.warehouse, transaction_typeout, quantityquantity, before_quantityinventory.quantity quantity, after_quantityinventory.quantity, operatoroperator, remarkremark, ) return inventory这里的select_for_update()把对应库存记录锁住直到事务提交或回滚才释放。在并发对比下如果没有行锁100 次并发扣 1 件商品可能最终只扣掉几十次有了行锁请求会排队执行最终准确扣减 100 次。一个常见的误用是只做前置校验把数量判断放在事务外。举例来说如果你在调用这个函数之前先查了一次库存发现够再进入事务扣减两次请求会同时通过检查。正确的防线是事务内再次校验也就是上面代码里的if inventory.quantity quantity这是最低成本且最可靠的一道闸门。4.3 库存预警阈值判断与定时任务库存预警并不复杂核心是定义明确规则当inventory.quantity inventory.min_quantity时触发。实现上推荐用 Celery Beat 周期扫描而不是每次操作后检查——因为数据可能从后端脚本、导入任务等多个来源变更只靠写入时校验容易漏。from celery import shared_task from django.db.models import F from .models import Inventory shared_task def check_low_stock(): 扫描全部仓库找出低于库存下限的商品 low_stock_items Inventory.objects.filter( quantity__lteF(min_quantity) ).select_related(product, warehouse) for item in low_stock_items: send_alert( product_skuitem.product.sku, warehouseitem.warehouse.name, current_quantityitem.quantity, min_quantityitem.min_quantity, )F(min_quantity)是数据库层面的字段引用不会把值取到 Python 内存中避免查询期间拿到过期快照select_related通过 SQL JOIN 一次取到商品和仓库信息避免 N1 查询。若不想引入 Celery 这种重量级依赖Django 自带的管理命令python manage.py check_stock也能承担同样的扫描职责配合系统的 crontab 每半小时执行一次即可。三种方案的对比方便你根据团队实际情况选择方案实时性额外依赖适用场景操作后同步检查即时无单应用、流量小的内部系统Celery Beat 定时任务分钟级延迟celery broker多应用、需异步通知Django 管理命令 cron按 cron 周期无低频率、简单场景选择前先评估最大容忍的预警延迟。如果库存清零后 30 分钟才收到通知就来不及补货那就必须走 Celery 实时检查不做定时扫描。预警的通知渠道也要留扩展口子邮件、钉钉、企业微信机器人用策略模式接一层不要在任务里写死。5. 上线前必做的生产部署优化清单5.1 关闭 DEBUG 并用 Gunicorn 承载服务源码里默认的DEBUG True只能在开发环境用。生产环境一开用户就能看到堆栈详情和路径结构这在仓库系统这类内网应用中属于高危配置。把settings.py改成DEBUG False ALLOWED_HOSTS [wms.example.com, 10.0.0.10]然后使用 Gunicorn 启动应用gunicorn warehouse.wsgi:application -w 4 -b 127.0.0.1:8000 --timeout 120参数解释-w 4是根据 CPU 核数设定的 worker 数通常从2*CPU1起步--timeout 120防止某些报表请求执行时间过长被强杀。Gunicorn 不擅长处理静态文件生产环境最常见的组合是 Nginx 前置转发静态资源交给 whitenoise 或 Nginx alias 目录。如果团队服务器装了宝塔面板用它做 Django 反向代理也很顺手核心思路仍是 Gunicorn 监听本地端口、Nginx 转发外部请求。5.2 数据库索引和查询优化的两个微调有两个优化在仓库系统里收益最明显。第一库存表经常按 product 和 warehouse 组合查询联合唯一索引已经覆盖精确查询但如果你需要支持按仓库查所有低库存商品应该在 Inventory 表上再建立一个(warehouse, quantity)联合索引models.Index(fields[warehouse, quantity], nameidx_wh_qty)第二StockTransaction 表只做追加、不做修改查询通常按 product created_at 倒序。把ordering [-created_at]加在 Meta 里后分页查询就不会因为隐式排序而全表扫描。最后用一条 SQL 验证数据一致性SELECT w.name, COUNT(DISTINCT t.product_id) AS product_cnt, SUM(CASE WHEN t.transaction_type in THEN t.quantity ELSE 0 END) AS total_in, SUM(CASE WHEN t.transaction_type out THEN t.quantity ELSE 0 END) AS total_out FROM warehouse w JOIN stock_transaction t ON t.warehouse_id w.id GROUP BY w.id;这条 SQL 能快速核对每个仓库的历史入库总量与出库总量若与 Inventory 快照不一致说明事务逻辑有漏需要回溯流水。上线前跑一遍是成本最低的验收手段。本文还有配套的精品资源点击获取
返回列表