
简介基于Django的超市进销存销售管理系统毕业设计源码面向计算机专业毕业生与课程设计学生提供一套结构完整、可运行的Web项目范例。压缩包内共65个文件以39个Python源码文件、16个HTML页面模板为主附带SQL数据库脚本、配置文件、文本说明及Markdown文档覆盖后端逻辑、前端页面、数据初始化与项目说明整体结构清晰便于直接部署、快速跑通和二次开发。系统覆盖商品信息管理、进货/销售记录、库存预警、供应商与客户管理、账务统计等核心业务模块借助Django的MTV架构、ORM数据操作和后台管理功能可帮助理解企业级Web应用从数据模型到页面呈现的完整链路为后续功能扩展打下基础。资源整体仅65KB轻量易用目前已有103人学习下载。适合作为毕业设计参考、课程设计案例或Django框架实践项目学习者可从中掌握环境搭建、数据库设计、路由视图配置、模板渲染及功能模块拆分等关键技能积累真实项目经验。1. 为什么超市进销存用 Django 写从毕设选题到可落地的管理源码超市的进销存场景天然适合做毕业设计它既有商品、分类、供应商这些标准主数据又有销售单、采购单、盘点流水这类带时间维度的业务数据业务上闭环代码量又不会大到毕设周期撑不完。选 Python 和 Django 来做首先是因为 Django 自带 ORM、模板引擎和 Admin 后台四个核心模块——商品管理、库存管理、销售管理、账务统计——都能在不额外引入重型前端框架的情况下跑通。很多课程设计项目卡在“需求很大但代码很薄”而这个源码包给出了相反的方向manage.py负责入口app目录装业务逻辑templates放页面django_demo.sql直接给了 MySQL 初始化数据非常适合拿来拆解和二次开发。无论你是在选毕设题目还是已经拿到源码不知道从哪个文件开始读这篇文章都按实际开发顺序给你把每一步拆开。2. 数据模型与 MySQL 配置ORM 设计进销存表结构的关键决策2.1 从 django_demo.sql 反向读懂表关系项目自带的 MySQL 文件是django_demo.sql拿到源码后先把数据库环境恢复出来。MySQL 导入的两条命令mysql -u root -p -e CREATE DATABASE IF NOT EXISTS django_demo DEFAULT CHARACTER SET utf8mb4; mysql -u root -p django_demo django_demo.sql第一句先按utf8mb4建库避免中文商品名在导入后出现乱码第二句把源码附带的 SQL 导入。导入后执行SHOW TABLES;查看表清单如果列表里出现了app_product、app_category、app_sale_record这类以app_开头的前缀说明项目默认的 app 名就是app后续写模型和路由时都要按这个前缀对号入座。这类毕业设计源码的数据库文件通常不只建表还会预置一部分分类和商品数据。导入后先跑几条查询确认数据完整性比如SELECT COUNT(*) FROM app_product;如果返回 0说明 SQL 里只有表结构没有种子数据后面需要通过 Django Admin 或自己写脚本补录。这里有一个容易踩的坑直接用 Navicat 导入 SQL 时如果原文件里有DROP TABLE IF EXISTS语句会先删除同名的本地表操作前最好确认当前库可重建。2.2 Django 模型选型库存字段为什么不用 Float外键为什么用 PROTECT进销存系统的核心是商品表、分类表和流水表。以一个典型的商品模型为例# app/models.py from django.db import models class Category(models.Model): name models.CharField(max_length64, uniqueTrue, verbose_name分类名称) class Product(models.Model): name models.CharField(max_length128, verbose_name商品名称) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name所属分类) price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) cost models.DecimalField(max_digits10, decimal_places2, verbose_name进价) stock models.IntegerField(default0, verbose_name当前库存) warning_threshold models.IntegerField(default10, verbose_name库存预警阈值) class Meta: verbose_name 商品 ordering [-id]这里三个决策点值得展开第一on_deletemodels.PROTECT表示分类下还有商品时禁止直接删除分类强制先处理商品记录避免孤儿数据与其用 CASCADE 一次误删把整个分类的商品连带清空不如在这里加一道保护。答辩时如果被问到外键级联策略能讲出这一层差异比背概念更有说服力。第二价格用DecimalField而不是FloatField因为浮点运算在涉及金额时会出现精度误差进销存直接和财务挂钩不能用0.1 0.2 ! 0.3这种隐患。第三stock用整数并设置默认 0库存字段不应允许负数后续通过 Django 事务保证扣减操作在并发下也不会把库存扣穿。2.3 settings.py 数据库配置与迁移技巧切到 MySQL 数据库需要在settings.py中修改DATABASES配置。多数这类项目的配置放在config目录下也有直接把settings.py放在项目根目录的以源码包实际结构为准# config/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: django_demo, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }ENGINE固定为django.db.backends.mysqlDjango 通过它找到 MySQL 适配器OPTIONS里的charset指定 UTF-8 编码中文数据全靠这一项保证不乱码。由于项目已经带了django_demo.sql初始化库并不需要再执行makemigrations生成迁移文件但 Django 启动时会检查迁移状态所以常见做法是执行一次先把迁移记录同步掉python manage.py migrate --fake-initial python manage.py createsuperuser--fake-initial会让 Django 跳过已经存在于数据库中的表结构只记录迁移版本不会重复建表。createsuperuser用于创建 Admin 后台的管理员账号如果源码的 SQL 里已经包含了管理员数据可以直接用python manage.py changepassword admin重置密码不必重新建账号。台账与流水表的分工表名关键字段用途说明app_categoryid, name商品分类app_productid, name, category_id, price, cost, stock商品主数据与当前库存app_sale_recordid, product_id, quantity, amount, created_at销售流水记录每一次卖出app_purchase_recordid, product_id, quantity, amount, created_at采购入库流水app_stock_checkid, product_id, real_stock, diff, check_time盘点结果与保证差异商品表存当前库存流水表存每一次变动。后期核对账实时用流水表的SUM(quantity)与商品表的stock对照就能判断系统账面与真实仓库是否一致这也是进销存系统最基础的审计逻辑。3. 商品与库存 CRUD 实战视图、路由、模板三层怎么配合3.1 路由设计与视图函数拆分Django 的请求处理链路是urls - views - templates。项目根路由要把 app 的路由包含进来app 内部再维护一份自己的路由表# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(app.urls)), ]# app/urls.py from django.urls import path from . import views urlpatterns [ path(, views.product_list, nameproduct_list), path(product/create/, views.product_create, nameproduct_create), path(product/int:pk/edit/, views.product_edit, nameproduct_edit), path(product/int:pk/delete/, views.product_delete, nameproduct_delete), path(category/int:pk/products/, views.category_products, namecategory_products), ]name参数是 URL 反向解析的依据模板里写{% url product_create %}而不是硬编码/product/create/这样以后改路由路径页面不用跟着改。int:pk是路径参数转换器Django 会自动做整型转换并传到视图函数里。对应关系整理如下URL 路径视图方法请求方法业务含义/product_listGET商品列表与库存状态/product/create/product_createGET/POST新增商品/product/pk/edit/product_editGET/POST编辑商品/product/pk/delete/product_deletePOST删除商品/category/pk/products/category_productsGET按分类筛选商品3.2 视图中 POST 处理与字段校验新增商品是进销存系统最基础的写入操作视图里不能拿到 POST 数据就直接create要做字段校验和异常处理# app/views.py from django.shortcuts import render, redirect, get_object_or_404 from django.views.decorators.http import require_POST, require_http_methods from .models import Category, Product require_http_methods([GET, POST]) def product_create(request): if request.method POST: name request.POST.get(name, ).strip() price request.POST.get(price, 0) cost request.POST.get(cost, 0) category_id request.POST.get(category_id) if not name: return render(request, app/product_form.html, {error: 商品名称不能为空}) try: price float(price) cost float(cost) except ValueError: return render(request, app/product_form.html, {error: 价格必须为数字}) Product.objects.create( namename, priceprice, costcost, category_idcategory_id, stock0, ) return redirect(product_list) categories Category.objects.all() return render(request, app/product_form.html, {categories: categories})require_http_methods([GET, POST])限定这个视图只接受两种请求方式其他请求直接返回 405省去在代码里写一堆if request.method判断。request.POST.get(name, ).strip()先给默认值再去空格避免用户提交纯空格字符串进入数据库。price float(price)在这里是快速校验Django 模型里的DecimalField在写入时还会做一次更严格的类型转换双保险。这里的category_id直接传给外键字段是 Django 的一种写法等价于先查出Category对象再赋给product.category省一次数据库查询代码也更短。3.3 模板侧CSRF token 与表单回显模板层的重点是表单安全而不是 UI 排版。一个能同时复用于新增和编辑的表单模板!-- templates/app/product_form.html -- form methodpost action{% if product %}{% url product_edit product.id %}{% else %}{% url product_create %}{% endif %} {% csrf_token %} input typetext namename value{{ product.name|default_if_none: }} placeholder商品名称 required input typetext nameprice value{{ product.price|default_if_none: }} placeholder售价 select namecategory_id {% for cat in categories %} option value{{ cat.id }} {% if product.category_id cat.id %}selected{% endif %}{{ cat.name }}/option {% endfor %} /select button typesubmit保存/button /form{% csrf_token %}是 Django 所有 POST 表单的强制要求缺少这个令牌视图会直接返回 403 Forbidden。表单的action根据product是否存在自动切换新增或编辑提交地址编辑时通过default_if_none把已有数据回显到输入框比在视图里拼 JSON 返回给前端简单得多。3.4 列表页查询优化用 select_related 避免 N 1商品列表页要显示分类名称模板里每访问一次product.category.nameDjangp 就执行一条关联查询100 个商品会变成 101 条 SQL页面越来越慢。加一个select_related就能通过 SQL JOIN 把关联数据一次性取出def product_list(request): products Product.objects.select_related(category).all() return render(request, app/product_list.html, {products: products})select_related适用于外键和一对一关系多对多关系则要用prefetch_related。毕业设计源码里如果能体现这个优化点即使在数据量不大时看不出性能差别也说明你理解 ORM 底层查询机制。删除操作限制为 POST 请求避免用户单纯点一个链接就把数据删掉require_POST def product_delete(request, pk): product get_object_or_404(Product, pkpk) product.delete() return redirect(product_list)get_object_or_404在商品不存在时直接抛 404省去手动判断exists()的代码。把删除限定为 POST是防止误删和恶意批量删除的常用手段。4. 销售与采购核心流程事务、并发扣减与库存预警4.1 一笔销售单的数据流进销存的业务核心不是增删改查而是保证「库存变动」和「流水记录」在同一个事务里完成。一笔销售单的动作是顾客购买、创建销售记录、减少商品库存这三个步骤不能拆开执行否则系统异常时会出现库存扣了但流水没生成或者流水生成了库存却没扣的情况。# app/views.py 销售下单 from django.db import transaction from django.http import JsonResponse from django.views.decorators.http import require_POST from django.shortcuts import get_object_or_404 from .models import Product, SaleRecord require_POST transaction.atomic def create_sale(request, product_id): product get_object_or_404(Product.objects.select_for_update(), pkproduct_id) quantity int(request.POST.get(quantity, 0)) if quantity 0: return JsonResponse({error: 销售数量必须大于 0}, status400) if product.stock quantity: return JsonResponse({error: f库存不足当前库存 {product.stock}}, status400) product.stock - quantity product.save(update_fields[stock]) SaleRecord.objects.create( productproduct, quantityquantity, amountproduct.price * quantity, ) return JsonResponse({code: 0, stock: product.stock})transaction.atomic把整个函数包进一个数据库事务函数内任何一步抛出异常前面已经执行的数据修改都会全部回滚。select_for_update()会对商品行加行级锁两个收银员同时操作同一商品时第二个请求必须等第一个事务提交才能执行从根上避免超卖。save(update_fields[stock])只更新 stock 字段而不是整个行减少不必要的数据库操作也避免并发时覆盖其他字段数据。4.2 采购入库与库存预警采购入库与出库的逻辑方向相反库存增加同时写采购流水。代码结构上与销售配对也用select_for_update保护并发transaction.atomic def purchase_in(request, product_id): product get_object_or_404(Product.objects.select_for_update(), pkproduct_id) quantity int(request.POST.get(quantity, 0)) if quantity 0: return JsonResponse({error: 入库数量必须大于 0}, status400) product.stock quantity product.save(update_fields[stock]) PurchaseRecord.objects.create( productproduct, quantityquantity, amountproduct.cost * quantity, ) return JsonResponse({code: 0, stock: product.stock})库存预警是进销存系统的高频需求。Django ORM 里用F表达式可以把比较运算下推到数据库端执行from django.db.models import F def low_stock_list(request): low_products ( Product.objects .filter(stock__lteF(warning_threshold)) .order_by(stock) ) return render(request, app/low_stock.html, {products: low_products})F(warning_threshold)表示直接引用数据库里当前行的这个字段stock__lteF(...)生成的 SQL 是WHERE stock warning_threshold在数据库里完成比较。如果换成把数据全部加载到 Python 再循环判断商品量大时性能会明显下降。这类查询在 Django 执行查询的官方文档里属于基础但高频的类型放在进销存场景里理解起来更直观。4.3 盘点调整与流水核对月底盘点时仓库实盘数可能和系统库存不一致。盘点调整操作是记录实盘数量、计算差异、更新库存、写盘点流水。transaction.atomic def stock_check_submit(request, product_id): product get_object_or_404(Product.objects.select_for_update(), pkproduct_id) real_stock int(request.POST.get(real_stock, 0)) diff real_stock - product.stock product.stock real_stock product.save(update_fields[stock]) StockCheck.objects.create( productproduct, real_stockreal_stock, diffdiff, ) return JsonResponse({code: 0, diff: diff})这里的关键是diff同时支持正负两个方向——实盘数大于账面差异为正说明之前少记了入库实盘数小于账面差异为负说明商品可能丢失或出库未记录。把差异直接记入盘点流水表后面做库存分析时能追溯到每一次库存调整原因。不同业务场景对库存的影响方向业务场景操作动作库存变化对应流水表销售出库创建 SaleRecord减少app_sale_record销售退货创建退货记录增加app_sale_return采购入库创建 PurchaseRecord增加app_purchase_record盘点调整创建 StockCheck调整为实盘app_stock_check记住一个原则不要在库存字段上直接单方面改数值而不留流水。进销存系统的审计价值全部体现在流水上答辩时如果能主动讲出「每一笔库存变动都要有流水支撑」项目深度会明显提升一个等级。提示这里的事务控制不能只写transaction.atomic就结束还需要配合select_for_update使用单靠事务隔离级别并不足以完全避免并发扣减库存的超卖问题。5. 部署与验证用 Admin 后台和日志确认系统跑通5.1 源码包结构与本地启动顺序源码里有manage.py、requirements.txt、deploy和logs目录启动顺序先装依赖再跑服务pip install -r requirements.txt python manage.py runserver 0.0.0.0:8000如果requirements.txt里锁定的版本与当前 Python 版本不兼容pip 会报编译错误常见处理方式是降低 Django 版本或者升级 Python 小版本。日志目录在项目里是logs/说明源码设计时预留了日志落盘的位置后面配置LOGGING时可以直接指向这个目录不需要再建。5.2 Django Admin 后台定制不写前端也能维护数据Django 自带的 Admin 后台是毕业设计里最容易出效果的模块在不写任何前端代码的情况下完成数据的录入和管理# app/admin.py from django.contrib import admin from .models import Product, Category, SaleRecord admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display (name, category, price, stock, warning_threshold) search_fields (name,) list_filter (category,) def mark_stock_zero(self, request, queryset): queryset.update(stock0) mark_stock_zero.short_description 将选中商品库存置 0list_display控制列表页显示的字段search_fields自动生成搜索框list_filter在侧边栏生成筛选器这些配置都是声明式写法改动即时生效。actions可以注册自定义批量操作比如一次把多个商品库存清零这种操作在生产环境要谨慎但在课程设计和功能演示中非常实用。5.3 事务失效的验证方法写完事务代码后不能只看一次请求成功就认为逻辑正确。最简单有效的验证方法是模拟并发扣减打开两个浏览器窗口同时提交同一商品的销售单如果库存被扣成负数或两个请求都返回成功说明select_for_update没有真正生效需要检查数据库表引擎是否为 InnoDB —— MyISAM 引擎不支持行级锁。在 settings.py 里配置日志输出观察请求的完整过程# config/settings.py 日志配置片段 LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: INFO, class: logging.FileHandler, filename: logs/app.log, } }, loggers: { app: {handlers: [file], level: INFO}, }, }在销售视图里加入日志输出import logging logger logging.getLogger(__name__) # 在 create_sale 中 logger.info(sale product_id%s quantity%s resultsuccess, product_id, quantity)通过logs/app.log里同一条商品多行销售记录的时间戳可以直接看到行锁生效时请求之间是依次完成的。这个验证方法比单纯看页面提示靠谱得多也能帮你在调试接口问题时快速定位是业务逻辑错还是数据库层的问题。如果后续要部署到 Linux 服务器参考deploy目录里的配置配合 Nginx 和 Gunicorn 即可本地开发阶段先保证日志能落盘、事务能回滚系统才算真正跑通。本文还有配套的精品资源点击获取