简介:基于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_length=64, unique=True, verbose_name="分类名称") class Product(models.Model): name = models.CharField(max_length=128, verbose_name="商品名称") category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name="所属分类") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="售价") cost = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="进价") stock = models.IntegerField(default=0, verbose_name="当前库存") warning_threshold = models.IntegerField(default=10, verbose_name="库存预警阈值") class Meta: verbose_name = "商品" ordering = ["-id"]这里三个决策点值得展开:第一,on_delete=models.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.mysql,Django 通过它找到 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_category | id, name | 商品分类 |
| app_product | id, name, category_id, price, cost, stock | 商品主数据与当前库存 |
| app_sale_record | id, product_id, quantity, amount, created_at | 销售流水,记录每一次卖出 |
| app_purchase_record | id, product_id, quantity, amount, created_at | 采购入库流水 |
| app_stock_check | id, 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, name="product_list"), path("product/create/", views.product_create, name="product_create"), path("product/<int:pk>/edit/", views.product_edit, name="product_edit"), path("product/<int:pk>/delete/", views.product_delete, name="product_delete"), path("category/<int:pk>/products/", views.category_products, name="category_products"), ]name参数是 URL 反向解析的依据,模板里写{% url 'product_create' %}而不是硬编码/product/create/,这样以后改路由路径,页面不用跟着改。<int:pk>是路径参数转换器,Django 会自动做整型转换并传到视图函数里。
对应关系整理如下:
| URL 路径 | 视图方法 | 请求方法 | 业务含义 |
|---|---|---|---|
/ | product_list | GET | 商品列表与库存状态 |
/product/create/ | product_create | GET/POST | 新增商品 |
/product/<pk>/edit/ | product_edit | GET/POST | 编辑商品 |
/product/<pk>/delete/ | product_delete | POST | 删除商品 |
/category/<pk>/products/ | category_products | GET | 按分类筛选商品 |
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( name=name, price=price, cost=cost, category_id=category_id, stock=0, ) 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 method="post" action="{% if product %}{% url 'product_edit' product.id %}{% else %}{% url 'product_create' %}{% endif %}"> {% csrf_token %} <input type="text" name="name" value="{{ product.name|default_if_none:'' }}" placeholder="商品名称" required> <input type="text" name="price" value="{{ product.price|default_if_none:'' }}" placeholder="售价"> <select name="category_id"> {% for cat in categories %} <option value="{{ cat.id }}" {% if product.category_id == cat.id %}selected{% endif %}>{{ cat.name }}</option> {% endfor %} </select> <button type="submit">保存</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, pk=pk) 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(), pk=product_id) quantity = int(request.POST.get("quantity", "0")) if quantity <= 0: return JsonResponse({"error": "销售数量必须大于 0"}, status=400) if product.stock < quantity: return JsonResponse({"error": f"库存不足,当前库存 {product.stock}"}, status=400) product.stock -= quantity product.save(update_fields=["stock"]) SaleRecord.objects.create( product=product, quantity=quantity, amount=product.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(), pk=product_id) quantity = int(request.POST.get("quantity", "0")) if quantity <= 0: return JsonResponse({"error": "入库数量必须大于 0"}, status=400) product.stock += quantity product.save(update_fields=["stock"]) PurchaseRecord.objects.create( product=product, quantity=quantity, amount=product.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__lte=F("warning_threshold")) .order_by("stock") ) return render(request, "app/low_stock.html", {"products": low_products})F("warning_threshold")表示直接引用数据库里当前行的这个字段,stock__lte=F(...)生成的 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(), pk=product_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( product=product, real_stock=real_stock, diff=diff, ) 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(stock=0) mark_stock_zero.short_description = "将选中商品库存置 0"list_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 result=success", product_id, quantity)通过logs/app.log里同一条商品多行销售记录的时间戳,可以直接看到行锁生效时请求之间是依次完成的。这个验证方法比单纯看页面提示靠谱得多,也能帮你在调试接口问题时快速定位是业务逻辑错还是数据库层的问题。如果后续要部署到 Linux 服务器,参考deploy目录里的配置,配合 Nginx 和 Gunicorn 即可,本地开发阶段先保证日志能落盘、事务能回滚,系统才算真正跑通。
本文还有配套的精品资源,点击获取