Django+MySQL超市进销存系统开发:模型设计、事务与报表

发布时间:2026/9/14 2:11:17
Django+MySQL超市进销存系统开发:模型设计、事务与报表 简介面向毕业设计与Python后端学习者的超市进销存销售管理系统源码基于DjangoMySQL实现覆盖商品、供应商、进货、销售、库存、报表等核心业务模块适合用于课程设计、毕业设计或入门Web开发实践。资源共64个文件以Python源码39个py、HTML模板16个html为主另含MySQL数据库脚本、项目配置文件、依赖清单及说明文档压缩包仅56KB结构紧凑便于快速部署与二次开发。项目采用Django框架的MVT模式通过内置ORM操作MySQL数据库前端页面配合Django模板渲染整体架构清晰便于理解业务逻辑与数据交互。目前已吸引59人学习浏览对希望掌握Django实际项目流程、理清进销存业务表设计及后台管理思路的读者具有直接参考价值。1. 为什么这套超市进销存用 Django MySQL 而不是其他技术组合找我改毕业设计代码的人里十个有八个是进销存商品管理、进货、销售、库存预警、报表。翻车原因高度一致——数据库字段建得随意库存靠手算销售单和进货单是一张平表最后对不上账。这个 Django MySQL 超市进销存项目结构上是标准的单据主从表库存字段冗余在商品表中通过事务维护进货和销售各走一套明细报表用 ORM 聚合实现不依赖裸 SQL。适合做课程设计也适合想了解 Django 业务系统如何落地的后端开发者。先说破一点这类系统的核心不是页面交互而是单据、库存、金额三个数字在极端操作下保持一致。用户能忍页面丑忍不了月底对不上账。所以下面重点全在 Model 设计、事务边界和查询聚合上。2. Django 数据模型设计与 MySQL 表结构规划2.1 从业务实体到 Django Model 的映射先把超市后台的实体拆开供应商、商品分类、商品、进货单、进货明细、销售单、销售明细。Supplier 和 Category 是基础档案Product 是核心业务表单据用“主表 明细表”的结构。我习惯先画一张关系图再写代码Supplier 对 Product 是一对多Category 对 Product 是一对多StockInSheet 对 StockInItem 是一对多SaleSheet 对 SaleItem 也是一对多。models.py 的骨架如下from django.db import models class Supplier(models.Model): name models.CharField(供应商名称, max_length100, uniqueTrue) contact models.CharField(联系人, max_length30, blankTrue) phone models.CharField(联系电话, max_length20, blankTrue) class Category(models.Model): name models.CharField(分类名称, max_length50, uniqueTrue) class Product(models.Model): name models.CharField(商品名称, max_length100) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name分类) supplier models.ForeignKey(Supplier, on_deletemodels.PROTECT, verbose_name供应商) sales_price models.DecimalField(销售单价, max_digits10, decimal_places2) stock models.IntegerField(当前库存, default0) low_stock models.IntegerField(库存预警线, default10) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: indexes [models.Index(fields[name])] db_table product class StockInSheet(models.Model): supplier models.ForeignKey(Supplier, on_deletemodels.PROTECT, verbose_name供应商) in_date models.DateTimeField(入库时间, auto_now_addTrue) total_amount models.DecimalField(进货总额, max_digits12, decimal_places2, default0) class StockInItem(models.Model): sheet models.ForeignKey(StockInSheet, on_deletemodels.CASCADE, related_nameitems, verbose_name进货单) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name商品) quantity models.IntegerField(进货数量) cost_price models.DecimalField(进货单价, max_digits10, decimal_places2) class SaleSheet(models.Model): sale_no models.CharField(销售单号, max_length32, uniqueTrue) sale_date models.DateTimeField(销售时间, auto_now_addTrue) total_amount models.DecimalField(销售总额, max_digits12, decimal_places2, default0) class SaleItem(models.Model): sheet models.ForeignKey(SaleSheet, on_deletemodels.CASCADE, related_nameitems, verbose_name销售单) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name商品) quantity models.IntegerField(销售数量) unit_price models.DecimalField(成交单价, max_digits10, decimal_places2)几个选型点解释一下。金额字段一律用 DecimalField不要用 FloatField。0.1 0.2 在二进制浮点里得不到 0.3销售金额一旦涉及折扣和退货叠加浮点误差会滚进账目。DecimalField 把精度交给数据库十进制运算max_digits 是总位数decimal_places 是小数位10 位总长 2 位小数对超市单笔交易足够。外键的 on_delete 属性决定了关联数据删除时的行为。Product 里的两个外键都用 PROTECT只要还有商品属于这个分类或者这个供应商分类和供应商就不能被删。单据明细也用 PROTECT防止删商品时把历史销售连带删掉。StockInItem / SaleItem 的外键却用 CASCADE因为主单删除时明细没有存在意义这是主从表的常见做法。还有一点容易忽略Product.stock 是冗余字段。理想化设计是库存只从明细表实时汇总但超市后台要频繁展示库存数字每次 SUM 两张明细表代价太高所以工程上普遍把库存冗余到商品表用事务保证一致性。这是后面所有库存操作的基石。如果是新起业务而不是直接打开现成源码包先执行 python manage.py startapp core 创建业务模块再把 app 加到 INSTALLED_APPS之后所有模型都写在 core/models.py 里。2.2 单据主从表结构和数据库约束主从表把“每笔交易的摘要”和“每件商品的流水”分开存储。StockInSheet 只存供应商、时间、总额StockInItem 存这次进了哪些商品、各多少件、什么单价。对账时按主表查总额分析销量时从明细表聚合互不干扰。销售表结构同理。表关系整理成下表后边写查询时照着这张表找字段即可数据表关键字段关联关系设计说明suppliername, contact, phone被 Product.supplier 引用供应商档案categoryname被 Product.category 引用商品分类productname, sales_price, stock, low_stockFK 到 category/supplier商品主档stock 是冗余库存stock_in_sheetsupplier, total_amountFK 到 supplier进货单主表stock_in_itemproduct, quantity, cost_priceFK 到 sheet/product进货明细保留当时进价sale_sheetsale_no, total_amount无销售单主表sale_no 唯一sale_itemproduct, quantity, unit_priceFK 到 sheet/product销售明细保留成交价注意到 sale_sheet 没有外键到供应商或客户原因是超市零售对客户信息不敏感摘要里也不需要客户维度如果做成会员制再补 customer_id 即可。sale_no 用唯一约束生成规则建议“日期 随机数”或数据库自增不要用时间戳做单号同一秒两笔单号会撞。生产环境不建议直接拿 Django migrate 建全库而是先建好库和账号CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER supermarketlocalhost IDENTIFIED BY 密码; GRANT ALL PRIVILEGES ON supermarket.* TO supermarketlocalhost; FLUSH PRIVILEGES;数据库字符集用 utf8mb4商品名万一有 emoji 或生僻字也能存下。接着修改项目的 settings.pyDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: supermarket, USER: supermarket, PASSWORD: 密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }2.3 迁移执行和 mysqlclient 驱动问题确认数据库和账号后执行迁移同时让 Django 内置 Admin 的表一并生成pip install -r requirements.txt python manage.py makemigrations --name init_models python manage.py migrate python manage.py createsuperusermakemigrations 对比模型和已有迁移文件生成新的迁移文件migrate 真正把迁移文件落到 MySQL。之后每次改模型都要重新跑这两条命令如果直接在 MySQL 里手改表结构下次 migrate 就会报目标状态和迁移记录不一致的错误。Python 连 MySQL 还需要驱动。requirements.txt 里最常出现的方案是 mysqlclient装好即可无缝对接 MySQLdb 接口如果源码编译报错备选方案是用 PyMySQL在项目包的init.py 里加 pymysql.install_as_MySQLdb()Django 会把 MySQLdb 调用转发给 PyMySQL。注意 MySQL 8 默认的 caching_sha2_password 认证方式可能让旧版驱动报 “Authentication plugin cannot be loaded”处理方式是升级驱动版本或把用户认证改回 mysql_native_password。3. 进货、销售与库存联动的事务实现3.1 进货单提交一个事务把库存加上去进库操作看起来是“填一张进货单库存加进去”实际要写三处数据进货主表、进货明细、商品表的库存。三处任何一步失败都不能让另外两处提交所以事务是硬需求。把业务写成独立函数不写在视图里这样测试和管理命令都可以调用。from django.db import transaction from .models import StockInSheet, Product def create_stock_in(supplier, item_list): with transaction.atomic(): sheet StockInSheet.objects.create(suppliersupplier) total 0 for item in item_list: sheet.items.create( productitem[product], quantityitem[quantity], cost_priceitem[cost_price], ) product Product.objects.select_for_update().get(pkitem[product].pk) product.stock item[quantity] product.save(update_fields[stock]) total item[quantity] * item[cost_price] sheet.total_amount total sheet.save(update_fields[total_amount])transaction.atomic() 开启事务块块内任何异常都会整体回滚进货明细不会留下半张单。select_for_update() 把对应商品的行锁住直到事务结束锁的作用是防止两个操作员同时给同一商品入库时互相把 stock 覆盖。进货总金额在服务端算不接收前端传来的 total_amount前端参数只能被当作参考。product.save(update_fields[stock]) 只提交 stock 一列避免整行数据不必要的写操作。如果 item_list 很大可以先在事务外试算总额再循环写库锁持有时长会短一些。3.2 销售出库库存不足怎么拦截销售是反向操作先扣库存再写销售单和明细。这里比进货多了一个校验步骤库存不够时整单拒绝不允许负库存出库。from django.db import transaction from .models import SaleSheet, Product def create_sale(sale_no, item_list): with transaction.atomic(): sheet SaleSheet.objects.create(sale_nosale_no) total 0 for item in item_list: product Product.objects.select_for_update().get(pkitem[product].pk) if product.stock item[quantity]: raise ValueError(f{product.name} 库存不足剩余 {product.stock}) product.stock - item[quantity] product.save(update_fields[stock]) sheet.items.create( productproduct, quantityitem[quantity], unit_priceitem[unit_price], ) total item[quantity] * item[unit_price] sheet.total_amount total sheet.save(update_fields[total_amount])这段逻辑和进货高度对称但库存判断必须在 select_for_update 之后。先查库存再锁行会有并发窗口两个请求同时读到剩余 5 件库存各自都认为能卖 5 件最后库存变成 -5。锁住行再重新读到的数量才是那一刻的真实数量。库存不足抛出的 ValueError 会被视图层捕获转换成 JSON 提示给收银端。item_list 里每个元素是一个 dict包含 product 实例或主键、quantity、unit_price调用之前由视图层负责清洗参数不能直接信任 POST 原始数据。一个容易被忽略的点事务里不要做耗时操作。出货场景如果后面接支付回调、会员短信之类的逻辑应该先把库存和单据数据落库提交再异步处理外围动作否则事务长时间持锁会导致其他销售请求排队。提示如果视图把 create_sale 放在 HTTP 请求的同步流程里外部接口调用支付、短信通知务必放到事务提交之后。锁的持有时间越长收银高并发时排队的请求越多。3.3 库存预警和组合查询库存预警不是复杂算法本质是筛出“当前库存小于等于预警线”的商品from django.db.models import F def low_stock_products(limit50): return Product.objects.filter( stock__lteF(low_stock) ).select_related(category, supplier)[:limit]F(low_stock) 生成 SQL 层比较把 stock 字段和 low_stock 字段在数据库里直接比不需要把每行商品加载到 Python 再比较数据量大时性能差别明显。select_related 把外键查询合并成 JOIN取 50 条预警商品不会产生多余的 SQL属于 Django 查外键的基本功。limit 参数控制最多返回多少条页面展示和后台定时任务可以复用同一个函数。同一个查询可以直接用在视图和后台命令。按商品名模糊搜索时用 filter(name__icontains可乐)但前导通配符会使普通索引失效表到十几万行以后建议再看 MySQL 全文索引或分词方案超市几千个 SKU 的规模用不上。日常监控可以挂一个 cron把低库存商品写进日志或钉钉告警凌晨跑一次就够。4. 报表统计与 Django Admin 后台定制4.1 用 ORM 聚合做销售排行和日报报表部分不必写裸 SQL 或存储过程Django ORM 的 annotate aggregate 足够处理日报和排行。“今日 TOP10 商品”用一次查询就能完成from django.db.models import Sum, F, DecimalField from django.utils import timezone def today_rank(): return SaleItem.objects.filter( sheet__sale_date__datetimezone.localdate() ).values(product__name).annotate( total_qtySum(quantity), total_amountSum(F(quantity) * F(unit_price), output_fieldDecimalField()) ).order_by(-total_qty)[:10]values(product__name) 指定分组维度annotate 计算每个分组的总量和金额order_by(-total_qty) 按数量降序[:10] 只截取前十。sheet__sale_date 是跨表 JOIN 查询Django 会自动处理从 SaleItem 到 SaleSheet 的连接。output_field 表示结果类型金额求和用 DecimalField 输出避免类型推断错误。“近 7 天销售趋势”按天分组用 TruncDate 把销售时间截断到天from django.db.models import Sum, Count from django.db.models.functions import TruncDate from django.utils import timezone def weekly_sales(): start timezone.localdate() - timezone.timedelta(days6) return SaleSheet.objects.filter( sale_date__date__gtestart ).annotate( dayTruncDate(sale_date) ).values(day).annotate( amountSum(total_amount), order_countCount(id) ).order_by(day)筛选条件是 sale_date__date__gte用当前日期减 6 天作为起点。annotate(dayTruncDate(sale_date)) 先生成截断后的日期列再按它分组SQL 翻译出来是 DATE(sale_date)对千万级以下的数据量足够快。order_count 用 Count(id)一张销售单记一单。返回的结果可以通过 JsonResponse 喂给前端图表也可以用模板直接渲染表格。报表这部分最容易犯的错是拿 Python 循环拼 SUM每条商品一行查询最后拼一个数字数据量一大页面就卡住。聚合一次交给数据库完成既省带宽也省内存。4.2 Admin 后台的操作优化Django Admin 是这套系统最省事的后台入口把模型注册进去就自带增删改查。原生界面不算好看但胜在功能完整先加表格列、筛选和搜索再考虑界面美化。from django.contrib import admin from .models import Product admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display (name, category, supplier, sales_price, stock, low_stock) list_display_links (name,) list_filter (category, supplier) search_fields (name,) list_editable (sales_price, stock, low_stock) ordering (-stock,) admin.action(description清零选中商品库存) def clear_stock(self, request, queryset): queryset.update(stock0)list_display 控制列表里显示的列list_editable 让这些列直接在列表页修改核对价格时不用逐条点进详情。list_display_links 里不能再有 list_editable 的字段两个配置字段重叠会报错这是一个高频踩坑点。ordering (-stock,) 让库存多的排前面店长一眼看到积压商品。新版 Django 推荐用 admin.action 装饰器代替 actions 属性批量操作的可读性更高。原生 Admin 还有一个信息量很大的问题库存变更后没有操作日志想看谁在什么时间改了价格只能靠数据库审计。常见做法是引入 django-simple-history或者自己在 save_model 里写一条变更记录到独立的 log 表。不想加依赖就重写模型 save 方法把旧值快照存起来。界面美化方面可以换 SimpleUI也可以直接覆盖 admin/base_site.html 改标题和样式对进销存这种内部工具业务字段的可见性比视觉重要得多。注意Admin 是内部管理工具不适合直接暴露到公网。真要做对外接口用 Django REST Framework 单独开 API 层。课程设计提交验收时 Admin 已经完全够用不用为了“看起来高级”去硬套前后端分离架构。5. 部署到服务器后的功能验证与依赖排错5.1 造数脚本验证业务闭环部署完第一件事不是看页面而是造一笔完整业务验证“进货增加库存销售扣减库存”的闭环。manage.py shell 里跑一段脚本from core.models import Supplier, Category, Product from core.services import create_stock_in, create_sale sup Supplier.objects.create(name本地测试供应商) cat Category.objects.create(name饮料) prod Product.objects.create(name可乐, categorycat, suppliersup, sales_price3.5, stock0, low_stock10) create_stock_in(sup, [{product: prod, quantity: 100, cost_price: 2.0}]) assert prod.stock 100 create_sale(T20250101001, [{product: prod, quantity: 3, unit_price: 3.5}]) prod.refresh_from_db() assert prod.stock 97脚本通过 create_stock_in 进货 100 件再 create_sale 卖 3 件最后从数据库重读商品判断库存是不是 97。任何一步抛异常说明事务、字段或外键配置有问题而不是页面跑起来就算成功。断言通过后再进入 Admin 看数据是否一致。5.2 时区、静态文件和 MySQL 连接上线后容易翻车的三个点时区、静态文件、数据库连接。USE_TZ 为 True 时 Django 往 MySQL 里存的是 UTC 时间报表里“今天”要用 timezone.localdate() 而不是 date.today()否则每天早上 8 点前统计的报表会少一天。settings.py 里设置 TIME_ZONE Asia/Shanghai模板渲染时用 Django 自带的本地时间过滤器。DEBUG 改成 False 后 Admin 的样式会全部丢失因为静态文件不再由开发服务器托管。必须执行 python manage.py collectstatic并保证 Nginx 的静态目录指向 STATIC_ROOT。用宝塔面板部署时在 Python 项目管理器里创建虚拟环境、安装 requirements.txt站点配置选择 uwsgi 或 gunicornNginx 反代到 127.0.0.1:8000再把静态目录指到 collectstatic 的输出位置Admin 的 CSS 和 JS 才能正常加载。MySQL 并发连接和数据备份也要提前安排。Django 和 MySQL 的连接在反向代理空闲超时会自动恢复但并发数突然涨到几百时要检查 max_connections 配置。备份用定时任务最省心mysqldump -u root -p supermarket | gzip /data/backup/supermarket_$(date %F).sql.gz凌晨 2 点执行一次。进销存的单据流水是核心资产系统可以重建交易流水绝对不能丢。本文还有配套的精品资源点击获取