Django进销存系统实战:从源码解析到跑通入库出库完整闭环

发布时间:2026/10/9 18:07:03
Django进销存系统实战:从源码解析到跑通入库出库完整闭环
简介面向Python/Django学习者的一份完整项目源码基于Django框架实现商品销售进销存管理涵盖商品信息维护、采购入库、销售出库、库存预警等典型业务环节适合作为期末大作业、课程设计或毕业设计的参考。压缩包共2000个文件其中JavaScript与HTML、CSS三类前端文件占据绝大多数用于构建系统页面、表格、表单及交互效果另含少量Python源码、JSON配置或数据文件、XML和文本说明等整体大小仅6.08MB。目前已有124人学习下载。资源附带完整源码与数据库文件评审分达95分以上经过严格调试可直接运行前端采用Bootstrap等框架页面风格统一阅读Python源码可快速理解Django的模型、视图、模板与路由组织方式无论是复习知识点还是改造项目都能节省大量从零搭建的时间。资源目录按功能模块划分结构清晰便于定位采购、销售、库存对应代码特别适合作为课程答辩的演示项目也可帮助初学者快速掌握进销存系统的数据流转与后端开发思路。1. 一套拿来即用的Django进销存先搞清楚你拿到的是什么在你双击那份 zip 之前先想一个问题进销存系统最难的从来不是增删改查而是数据一致性——库存扣了但单据没保存或者单据保存了库存却没动这两个问题在 Django 项目里能折腾人一整天。标题里这套基于 Django 的商品销售进销存系统价值不在页面数量而在于它把商品建档、采购入库、销售出库、库存台账串成了一条完整业务闭环数据库文件里还带了一笔可直接开箱演示的初始数据。它适合两类人一类是课程设计或毕业设计需要可演示、可答辩的 Python Web 项目另一类是初学 Django、想看清楚业务状态到底怎么流转的开发者。你不需要从零建模但需要能读懂它、跑通它、讲得清它。这套方向值不值得投入看完后面几章的拆解和避坑你会自己给出答案。2. 拆开源码包Django进销存项目的目录结构与三张核心表2.1 先分清 zip 里的三块内容工程代码、数据库文件、说明文档拿到 zip 别急着runserver先按“是不是 manage.py 所在目录”把它分类。这类项目解压后通常会出现两类东西一层是 Django 工程目录里面有manage.py、包含settings.py的配置目录以及按功能拆分的 app 包另一层是独立的.sqlite3或.sql数据库文件可能还有 README 或需求文件。先确认数据库文件的位置再决定怎么接这决定了你后面是“十分钟跑通”还是“修两小时数据库”。# macOS / Linux 下先在解压目录执行优先定位关键文件 find . -maxdepth 3 \( -name manage.py -o -name *.sqlite3 -o -name *.sql \) # 或者安装 tree 后直接看整体结构 tree -L 2 -I __pycache__逻辑说明find的目的是确认工程根目录和数据库文件路径两条命令一条负责找入口、一条负责找数据。如果压缩包里同时出现db.sqlite3和带migrations目录的 app说明作者期望“直接打开就能演示”如果只有.sql文件则说明数据需要手动导入 MySQL 之类的服务型数据库那就不能靠runserver一步到位。Windows 用户不用纠结命令行直接在编辑器文件树里搜manage.py和sqlite3更省事。参数说明-maxdepth 3限定了搜索深度避免把venv里海量的第三方文件也翻出来-I __pycache__是让 tree 跳过缓存目录看结构时不会被干扰。我一般会把数据库文件单独复制到工程根目录再运行而不是让它躺在某个子目录里因为 Django 的DATABASES配置经常默认指向BASE_DIR / db.sqlite3路径对不上是头号翻车点。2.2 MVT 怎么落到进销存models 看数据、views 看流程、templates 看页面Django 的 MVT 在进销存项目里对应关系非常清楚models.py定义商品、库存、单据这些数据形态views.py写的是入库、出库、查询、统计这些业务分支templates只是把结果渲染成表格和表单。很多同学拿到项目第一件事是翻 views这是读代码顺序上最常见的错误——views 里抛异常往往是因为 models 或表单没对上。读这类项目正确的切入路径是先打开models.py把所有字段和ForeignKey关系画成一张图然后去看 URLconf 里每个路径对应哪个视图函数最后再看视图函数改了哪些状态字段。进销存项目的业务闭环全部体现在状态变化上采购入库让Stock.quantity增加销售出库让它减少StockLog流水表负责记录每次变化的来源单据。把这三个对象的关系理清了整个系统的骨架就出来了。还有一个容易被忽视的文件是admin.py。这类“高分项目”十有八九靠 Django Admin 后台撑起演示里面注册了哪些模型决定了你能不能在后台直接录入商品、补一张入库单。如果后台页面看起来简陋优先检查admin.py有没有把核心模型都注册进去很多功能不是没有实现而是根本没暴露出来。2.3 三张核心表的关系商品档案、库存台账、出入库流水我一般会在白纸上先画三张表的关系再打开源码对照。核心关系是商品Product一对一关联到库存Stock单据流水StockLog外键指向商品和单据号这样订单、库存、流水三个维度都能追溯。用代码表示大致是下面这个样子from django.db import models class Product(models.Model): # 商品档案所有单据都围绕这个表转 name models.CharField(商品名称, max_length128) sku models.CharField(商品编码, max_length64, uniqueTrue) unit models.CharField(计量单位, max_length16, default件) price models.DecimalField(参考价格, max_digits10, decimal_places2) low_stock models.IntegerField(低库存预警线, default10) def __str__(self): return self.name class Stock(models.Model): # 库存台账冗余一个当前数量查询时不用反复 SUM product models.OneToOneField(Product, on_deletemodels.CASCADE, related_namestock) quantity models.IntegerField(当前库存, default0) class StockLog(models.Model): # 流水表一条记录代表一次库存变动 TYPE_CHOICES ((in, 入库), (out, 出库)) product models.ForeignKey(Product, on_deletemodels.CASCADE, related_namelogs) order_no models.CharField(关联单号, max_length64, blankTrue) change models.IntegerField(变动数量) log_type models.CharField(类型, max_length8, choicesTYPE_CHOICES) created_at models.DateTimeField(发生时间, auto_now_addTrue)参数说明Stock用OneToOneField而不是直接在Product上加quantity字段是为了支持“有商品但从未入库”和“库存清零后仍保留档案”两种状态直接耦合字段会破坏这一点low_stock放在商品表而不是库存表对应业务上“不同商品不同预警线”的需求StockLog.change字段用有符号整数入库记正、出库记负配合log_type让统计查询既灵活又直观。逻辑说明为什么不每次从流水SUM出库存演示数据量小看不出差别数据一多每次打开商品列表都全表聚合性能很难看。库存台账里冗余一个quantity用事务保证它在每次入库出库时和流水同步更新这是进销存项目里最值得学习的设计之一也是后面第 4 章代码的核心前提。3. 本地跑通全流程环境准备、数据库接参与三个必改参数3.1 Python 与 Django 版本匹配先装对解释器再谈跑通这类课程设计项目大多基于 Django 2.x 到 4.x 时代的技术栈写成MVT 结构相对传统。建议直接用 Python 3.8 到 3.11 之间的稳定版本先确认解释器版本再创建虚拟环境避免把老项目跑进新解释器的兼容坑里。python --version # 建议 3.8 ~ 3.11太新的解释器会在少数老项目上爆兼容问题 python -m venv venv # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate pip install -r requirements.txt逻辑说明venv创建隔离环境避免污染系统 Python也让依赖关系可复现。requirements.txt存在时优先按它装装完用pip list | grep -i django确认 Django 版本。如果压缩包里没有这个文件或者版本冲突导致安装失败直接pip install django通常也能跑起来因为这类项目的核心依赖基本只有 Django 本身其余模块大多是标准库。参数说明Python 版本不是我随口说的Django 各版本对 Python 有明确要求老代码在新解释器上最常见的报错是语法层面和第三方扩展不兼容。如果你发现runserver报关于collections或MRO的异常先别怀疑业务代码优先换到 3.8/3.10 这种保守版本再试。3.2 数据库文件接入的两种方式直接用 db.sqlite3 还是重建标题既然注明了带数据库文件解压后第一件事就是先找到它可能叫db.sqlite3也可能叫xxx.sql。如果拿到的是 SQLite 文件先用它因为里面大概率已经放好了管理员账号、商品分类和初始单据演示效果和你自己空手建库完全不同。用法是把文件放到settings.py里DATABASES配置的 NAME 指向的路径通常就是工程根目录然后直接启动。# 方式一直接用现成的 sqlite3 文件 python manage.py runserver # 浏览器访问 http://127.0.0.1:8000/ 看能否打开登录页 # 方式二数据库文件不匹配或反复报错时重建 cp db.sqlite3 db.sqlite3.bak python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver逻辑说明方式一省去所有建库步骤前提是文件路径正确、模型和迁移历史一致。方式二相当于把原数据库文件当作参考自己重新生成一套适用于启动时报no such table或table already exists的情况。我一般会先把原文件备份成.bak再重建因为初始数据一旦丢了演示前还要手动录入商品和单据非常浪费时间。参数说明createsuperuser会交互式要求输入用户名、邮箱、密码。先记好你设置的管理员密码后面登录后台要用。如果你不确定原文件里有没有管理员账号先用方式一启动进入登录页后试createsuperuser会提示用户已存在这时改用changepassword重置后面 3.4 会具体演示。3.3 settings.py 三个必改参数与一个建议关闭项不管数据库走哪条路settings.py里这几个值决定了你能不能跑、跑起来是什么状态。改错任何一个都可能让你在“能登录但数据不对”和“页面 500”之间反复横跳# settings.py 片段 DEBUG True # 演示阶段保持 True提交部署前必须改 False DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, # 路径要和数据库文件实际位置一致 } } LANGUAGE_CODE zh-hans # 界面中文 TIME_ZONE Asia/Shanghai # 业务时区 USE_TZ False # 进销存建议关掉避免入库时间差 8 小时逻辑说明DEBUG决定报错页面是否展示完整调用栈NAME不对是“数据库文件明明存在但连不上”的第一号原因USE_TZ关闭后auto_now_add字段存的是本地时间统计月度报表不会跨时区错位。注意这几个值从左到右有依赖关系——只改TIME_ZONE不改USE_TZDjango 还是会按 UTC 存取时间等于白改。参数作用踩坑提示DEBUG调试模式开关改 False 后静态资源会全挂见第 5 章NAME数据库文件路径路径必须与实际文件位置完全一致LANGUAGE_CODE界面语言不设 zh-hans 会出现英文后台TIME_ZONE业务时区要和 USE_TZ 配合两者一起改USE_TZ是否启用 UTC进销存建议 False否则日志时间差 8 小时3.4 最小运行命令与登录验证跑通的标志不是“服务起来了”而是“登录后能看到商品列表或库存页面”。最小命令组合是python manage.py runserver 0.0.0.0:8000然后访问http://127.0.0.1:8000/admin/用管理员账号登录。如果你不知道原数据库里的密码不要猜直接重置python manage.py changepassword admin逻辑说明changepassword只接受用户名参数admin只是最常见的账号名。如果提示用户不存在先用下面的命令看清库里到底有哪些账号python manage.py shell -c from django.contrib.auth.models import User; print(User.objects.values_list(username, flatTrue))参数说明0.0.0.0:8000允许局域网内其他设备访问答辩时把笔记本和投影仪连同一个网络可以在别的机器上打开演示页面。常见情况是页面打开了但登录报错这时去看控制台输出的 SQL 或表单校验信息而不是怀疑账号密码错了——先把这步跑通后面业务验证才有意义。4. 核心业务这样实现采购入库、销售出库与库存台账的代码细节4.1 采购入库一个事务里完成单据创建与库存增加入库的本质是“先锁住商品行再改库存最后写流水”。这里的顺序不能乱先锁、再改、再写是为了防止两个请求同时对同一商品入库时互相覆盖。代码大致长这样from django.db import transaction from .models import Product, Stock, StockLog transaction.atomic def purchase_in(order_no, items): # items: [{sku: SKU001, qty: 10}, ...] logs [] for item in items: # 锁住商品行防止并发修改 product Product.objects.select_for_update().get(skuitem[sku]) qty int(item[qty]) # 商品从未入库时自动创建库存台账 stock, created Stock.objects.get_or_create(productproduct) stock.quantity qty stock.save() logs.append(StockLog(productproduct, order_noorder_no, changeqty, log_typein)) # 批量写流水减少数据库往返 StockLog.objects.bulk_create(logs) return order_no逻辑说明select_for_update()必须在事务里才生效它给查出来的商品行加锁第二个入库请求只能等第一个提交后才能继续。get_or_create针对的是“新建商品还没建档”的场景第一次入库时库存默认 0加完数量再保存。bulk_create批量插入流水数据量大时明显快于逐条create。参数说明items的每项只传sku和qty不要在函数里直接接收整个表单对象这样入库逻辑可以被后台表单、脚本、单元测试三种方式复用。如果你在项目里看到类似的函数没有transaction.atomic那就是隐患——中途任何一条商品抛异常前面的库存改动不会自动回滚。4.2 销售出库库存不足必须报错且别让错误留下半截数据出库比入库多一个校验动作库存不足时必须中断而且中断不能影响其他商品。做法是先在锁内检查再扣减再写流水。注意change字段存负数这样“当前库存 所有流水的 SUM(change) 初始值”在数学上自洽。transaction.atomic def sale_out(order_no, items): # items: [{sku: SKU001, qty: 3}, ...] for item in items: product Product.objects.select_for_update().get(skuitem[sku]) qty int(item[qty]) stock Stock.objects.select_for_update().get(productproduct) # 先校验后扣减校验和扣减必须在同一把锁里完成 if stock.quantity qty: raise ValueError(f商品 {product.name} 库存不足仅剩 {stock.quantity}) stock.quantity - qty stock.save() StockLog.objects.create(productproduct, order_noorder_no, change-qty, log_typeout)逻辑说明把“校验”和“扣减”放在同一个事务的同一把锁里是为了避免两个客户同时买走同一件商品——A 请求校验通过但还没提交B 请求也校验通过最终库存被扣成负数。raise ValueError会让事务回滚已经处理了一半的商品不会留下残数据这是这段代码最要紧的行为。参数说明change-qty的负号是很多初学着迷的地方。存负数而不是单独存“出库数量”字段好处是统计销售总量时直接-Sum(change)不用再按log_type分类加减。你自己写类似系统时务必遵守“入库正、出库负”这个约定否则第 4.3 节的报表代码会算翻。4.3 库存台账与聚合统计用 ORM 分组而不是写原生 SQL演示最容易被追问的就是“这个月卖了多少、利润多少”。在 Django 里做月度统计用 ORM 聚合比写原生 SQL 更安全也更容易改条件。这里的坑在符号因为出库流水里change是负数算数量时要记得取反。from django.db.models import Sum, F from .models import StockLog def month_sales(month): # 统计某月度每个商品的销售总量与销售总额 rows (StockLog.objects .filter(log_typeout, created_at__monthmonth) .values(product__name) .annotate( total_qty-Sum(change), # 出库时 change 为负取反得正数 total_amount-Sum(F(change) * F(product__price)) ) .order_by(-total_qty)) return rows逻辑说明values(...).annotate(...)是 Django 聚合的固定组合先按商品名分组再对每组求和。F(change) * F(product__price)把价格乘到数量上算总额时不会再写 Python 循环逐条算。total_qty-Sum(change)里的负号是必须的——如果你写成Sum(change)得到的是一个负数总数页面会显示“本月销售 -12 件”这种低级错误在答辩现场很尴尬。参数说明created_at__monthmonth在 SQLite 里通过strftime实现逻辑上等价于 SQL 的WHERE strftime(%m, created_at) month。如果系统改用 MySQL这一行不需要改ORM 会自动切换方言这也是这类项目坚持用 Django ORM 而不是裸 SQL 的重要原因。4.4 低库存预警与模糊搜索演示时最容易被追问的两个功能高分的演示通常不只是列表页还有“搜索商品”和“库存预警”这类一眼能看出业务价值的细节。两个功能加起来不到十行但能直接回答评审的追问“有没有库存预警能不能按名称搜”from django.db.models import Q, F from .models import Product keyword request.GET.get(q, ) # 按名称或编码模糊搜索Q 对象把两个条件拼成 OR products Product.objects.filter( Q(name__icontainskeyword) | Q(sku__icontainskeyword) ) # 当前库存低于预警线的商品 low_stocks Product.objects.filter(stock__quantity__ltF(low_stock))逻辑说明Q对象是 Django 里构造 OR 条件的最干净方式直接name__icontainskeyword只能查一个字段要搜编码就必须用Q把两个条件合并。F(low_stock)表示“把预警线字段值拿到数据库端参与比较”这样stock.quantity__lt两边都在数据库里算不会把全表商品加载进 Python 再过滤。参数说明icontains里的i表示不区分大小写对商品字母编码搜sku时非常有用。如果你看代码时发现预警条件是if stock.quantity 10这种硬编码建议改成读取商品表的low_stock字段——硬编码在演示数据少的时候看不出来数据一多每件商品的预警线都一样显然不合理。5. 避坑Django进销存项目从解压到演示的 5 个高频问题5.1 现象runserver 启动后报 “no such table: app_stock”这是拿到带数据库文件的 Django 项目最常见的启动报错。原因通常是压缩包里的db.sqlite3是旧版本代码的数据库而源码里的models.py最近改动过迁移文件和应用模型对不上或者是有人不小心删了 app 下的migrations目录Django 认为自己从未建过表。解决步骤是先用showmigrations看迁移历史再决定是保留库还是重建# 先备份兜底用的后悔药 cp db.sqlite3 db.sqlite3.bak # 查看迁移状态grep 出还没应用的迁移 python manage.py showmigrations # 如果发现迁移文件本身缺失重置该 app 的迁移再重建 python manage.py makemigrations python manage.py migrate python manage.py runserver逻辑说明showmigrations输出的[X]表示已应用[ ]表示未应用。大部分no such table的本质是“迁移历史里没建过这张表”。如果重置迁移文件后仍然报错直接走第 3.2 节的方式二备份后删除旧库重建会牺牲初始数据但能保证跑通。5.2 现象数据库文件明明存在却提示 unable to open database file文件就在工程根目录但 Django 提示打不开。原因集中在三类settings.py里的NAME写的是绝对路径而文件实际被移走了SQLite 需要目录写权限macOS 或 Linux 下解压目录只读Windows 下数据库文件被资源管理器或同步盘占用。解决是先在 shell 里打印实际路径核对python manage.py shell -c from django.conf import settings; import os; print(os.path.abspath(settings.DATABASES[default][NAME]))得到路径后把数据库文件复制到该路径并确认目录可写。Windows 用户注意关闭所有正在浏览该目录的文件管理器窗口再重试。这一步排查比反复重启runserver有效得多。5.3 现象连续出库几次商品库存变成负数很多演示系统的库存会被点成负数评分瞬间掉档。原因大都是视图函数里只有“扣减”没有“校验”或者校验和扣减不在同一个事务锁里用户连续点击提交按钮时第一个请求还没扣完第二个请求已经读到了旧库存。解决分两层数据层用第 4.2 节的锁加校验表现层在视图函数里加一道快速检查给用户直观错误提示qty int(request.POST.get(qty, 0)) stock Stock.objects.get(product_idproduct_id) if qty stock.quantity: # 不进数据库直接回表单页提示 messages.error(request, f库存不足当前仅剩 {stock.quantity})逻辑说明表现层的这道检查是为了友好提示防不住并发所以数据库层的事务锁仍必须保留。两层叠加才不会出现“提示正常但库存被扣穿”的怪象。改完记得把已经为负数的商品手工修正别把坏数据留着做演示。5.4 现象流水时间比当前时间少了 8 小时录入商品后看流水列表发现created_at是凌晨而不是当天白天。原因是USE_TZ True时 Django 统一按 UTC 存时间Asia/Shanghai比 UTC 快 8 小时模板渲染时并没有转换回本地时区。解决分两步改配置再修存量数据# settings.py 两处一起改 TIME_ZONE Asia/Shanghai USE_TZ False# 存量错位数据批量加 8 小时 import datetime from django.db import models from .models import StockLog StockLog.objects.all().update(created_atmodels.F(created_at) datetime.timedelta(hours8))逻辑说明只改TIME_ZONE不改USE_TZ是最容易犯的半截子修改。对于进销存这种对“业务发生时间”敏感的系统直接关掉 UTC 最省心。修复存量数据时先确认现在库里的时间是否都是错的如果部分是正确时间要先按时间范围过滤再更新否则会把本来正常的数据改坏。5.5 现象为了演示“生产模式”把 DEBUG 改成 False静态资源全丢原因是 Django 内置服务器默认只在DEBUG True时托管静态文件改为False后 CSS、JS 全部 404页面变成纯文本。解决是如果只是答辩演示DEBUG True保持原样如果非要模拟线上效果用 Django 自带的--insecure参数在本地临时托管python manage.py runserver --insecure逻辑说明--insecure只适合本地调试线上部署还是要接 WhiteNoise 或反向代理来托管静态目录。对这份项目而言答辩现场最稳妥的做法就是保持DEBUG True把精力放在业务演示上而不是和静态文件较劲。6. 交作业前的三个闭环验证与一个数据重建习惯6.1 三个闭环实验从入库到出库到报表各测一次答辩前不要只点开页面看一眼按业务闭环做三个实验第一入库 10 件确认库存台账 10、流水新增一条in记录第二销售 6 件确认库存变 4、流水新增一条out记录且change为负第三继续出库 5 件确认系统报“库存不足”且数据库没有留下任何半截单据。第三个实验最重要它检验的就是第 4.1 和 4.2 节的事务回滚是否生效也是评审最喜欢现场验证的点。如果第三个实验失败说明视图函数或业务逻辑被改坏了优先检查库存校验是否在事务锁内。快速验证命令python manage.py shell -c from apps.inventory.models import Product, Stock; p Product.objects.first(); s Stock.objects.get(productp); print(p.name, s.quantity)6.2 用 dumpdata 重建初始数据而不是手动改数据库文件数据库文件是二进制不是给人手改的这是血泪经验。演示数据一旦被点乱或者你试了 5.3 节的负数场景后库已经脏了最干净的做法是用 Django 自带 fixture 把初始状态固化随时重来python manage.py dumpdata --indent 2 init_data.json python manage.py flush --noinput python manage.py loaddata init_data.json逻辑说明flush清空整库loaddata从 JSON 重建数据。把init_data.json和db.sqlite3一起保留等于给自己留了一颗后悔药——数据库文件可以随便折腾一份干净的初始数据随时能恢复。我自己跑这类项目的习惯是先把数据库文件当黑匣子整体还原跑通后再用dumpdata把初始数据固化成 JSON最后才动手改业务代码改坏了就 flush 一把重来而不是反复备份.sqlite文件。这套习惯比临时背代码管用得多希望帮到你。本文还有配套的精品资源点击获取