资讯动态

Python+Django超市销售系统毕设实战:从数据库设计到ECharts数据可视化全解析

发布时间:2026/10/20 12:57:30 来源:尧图企业网站定制
毕业设计做成一套完整的超市销售系统又要带个“能看的”数据可视化模块在Python技术栈里绕来绕去最后大多数人的落点都是Django。我手上刚好有一版“基于PythonDjango的超市销售系统的设计与数据可视化模块的实现”项目源码、文档、讲解视频都有但光有源码没用关键是你得真搞懂里面每个模块为什么要这么写答辩的时候才能站得住脚。这篇文章就把这套系统的整体设计思路、核心实现细节、可视化模块的落地方式以及我在调试过程中踩过的坑、整理出的排查经验一次性说清楚。这套系统适合谁一个是正在准备Python方向毕业设计、又不想用烂大街的管理系统题目的人另一个是想把Django后端能力和ECharts可视化结合起来写进简历的在校生。读完你应该能明确知道数据库表怎么设计不会在答辩时被问倒订单和库存的联动怎么保证不出错图表数据如何从后端聚合到前端渲染还有部署演示和答辩准备里的那些书本上讲不到的细节。1. 项目定位与选型思路为什么是PythonDjango而不是Flask或者后端前后端分离1.1 超市销售系统的核心需求范围先把这个系统的功能边界画清楚。超市销售系统听起来大但落到毕业设计粒度上其实就是围绕“进货-库存-销售-统计”这条业务主线把日常运营最关键的环节做成可操作的模块。我这边实现的版本包含商品管理分类、品牌、条码、进价售价、库存管理入库、出库、库存预警、收银开单购物车-结算-生成小票、会员与折扣、采购单管理、数据可视化看板以及基于角色的用户权限控制。可以不做哪些供应商对账、多门店、复杂促销规则、移动端小程序这些在毕设周期内做起来性价比很低容易被细节拖住。你要是想加分优先把数据可视化做深而不是盲目扩功能。评委看重的往往不是功能多而是你对自己实现过的每个点都能讲清楚。1.2 选型Django的三个决定性理由第一点是Django自带Admin后台。商品、分类、供应商这些基础数据用Admin管理可以省掉一大半前端页面的开发量模型一注册就能录入测试数据这对后期演示太关键了。你想想如果你连商品都是手动插数据库光准备演示数据就够你折腾半天的。第二点是自带的认证与权限体系。用户登录、Session管理、Group权限Django已经给你搭好了架子做收银员和店长不同角色的权限控制不需要从零写Token或者Cookie那套东西安全性也比自己拼出来的方案要靠谱。第三点是ORM对复杂查询的支撑。后面做数据可视化模块时按天分组聚合销售数据、统计各分类销量占比、算环比增长率Django的ORM配合annotate和TruncDate可以直接在Python层完成根本不用写裸SQL到处粘。顺便泼一盆冷水如果你更倾向Flask这种微框架不是不行但篇幅会多很多。本人在做这个项目的过程中Flask想做到同样的效果光登录认证、Admin扩展和数据聚合层这几个部分就要自己多写很多代码。除非你本身就想靠这个展示造轮子能力否则没必要在毕业设计里跟自己过不去。1.3 模块划分与数据流向这套系统我在结构上是按业务域拆分的而不是按文件类型硬拆sales_system项目配置目录apps/products商品与分类apps/inventory库存和采购apps/orders收银、订单、订单明细apps/members会员与积分apps/dashboard数据可视化看板apps/users自定义用户与权限应用模块的拆分原则很简单把一个业务实体放在一个应用里避免app之间互相import成环。比如商品被订单引用那我就在订单应用里通过ForeignKey指向products而不是为了“解耦”去建一大堆中间层。数据流向大概是这样的采购单入库会增加库存收银开单时先检查库存是否充足扣减库存并生成销售订单销售产生的数据回到底层表里最后由dashboard应用去聚合这些数据展示成图表。整个链路非常清晰答辩画数据流图时也很方便。2. 数据库设计与模型层实现细节2.1 核心表结构设计先说商品表。超市系统和一般电商系统最大的区别在于商品有“进价”和“售价”两个价格字段而且进价是敏感数据普通收银员不应该看到。我实现的字段大致是这样class Product(models.Model): barcode models.CharField(max_length32, uniqueTrue, verbose_name条码) name models.CharField(max_length128, verbose_name商品名称) category models.ForeignKey(Category, on_deletemodels.PROTECT) brand models.CharField(max_length64, blankTrue) purchase_price models.DecimalField(max_digits10, decimal_places2, verbose_name进价) sale_price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) stock models.PositiveIntegerField(default0, verbose_name当前库存) warning_threshold models.PositiveIntegerField(default10, verbose_name预警阈值) status models.BooleanField(defaultTrue, verbose_name上架状态) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue)有两个细节值得注意一是价格字段用DecimalField而不是FloatField。货币字段用浮点数做加法经过多次运算会出现0.30000000000000004这类误差到时候统计报表对不上账你查问题会想砸电脑。DecimalField配合decimal_places2数据库层面用固定精度存储总和计算才靠谱。二是barcode加了unique约束。超市的商品条码是唯一标识收银时通常直接扫码没有唯一约束容易出现“按条码查到了两条商品”的尴尬错误。设计表的时候把这种隐含约束加上比在代码里各种去重要省心得多。订单表的设计我采用了“主表明细表”的方式。主表存订单整体信息明细表存每一个商品条目class SaleOrder(models.Model): order_no models.CharField(max_length32, uniqueTrue) member models.ForeignKey(Member, nullTrue, blankTrue, on_deletemodels.SET_NULL) cashier models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT) total_amount models.DecimalField(max_digits12, decimal_places2, default0) discount_amount models.DecimalField(max_digits12, decimal_places2, default0) actual_amount models.DecimalField(max_digits12, decimal_places2, default0) created_at models.DateTimeField(auto_now_addTrue) class SaleOrderItem(models.Model): sale_order models.ForeignKey(SaleOrder, related_nameitems, on_deletemodels.CASCADE) product models.ForeignKey(Product, on_deletemodels.PROTECT) quantity models.PositiveIntegerField() price models.DecimalField(max_digits10, decimal_places2) # 下单时的快照价 subtotal models.DecimalField(max_digits12, decimal_places2)订单主表里存“实收金额”也存“折扣金额”是为了退货和统计毛利的时候能对得上。如果你不做会员折扣这两个字段可以合并成一个actual_amount但既然要做就尽量把字段留全避免后面改表结构改到怀疑人生。2.2 订单明细里为什么要有“价格快照”这条经验是我在实际跑数据时体会出来的。商品表的sale_price会被改价比如节假日促销调价调完之后历史订单明细如果只存product外键那就意味着历史订单的价格也跟着变了订单总额对不上账。正确的做法就是在SaleOrderItem里冗余一个price字段在下单那一刻把商品的当前售价复制过来。这样查询历史订单时看到的就是当时成交的价格不会被后续调价影响。这个概念在答辩时是一个很清晰的加分点直接说明你理解了“快照”在业务系统里的意义。2.3 外键级联策略的选择很多初学者图省事外键全部用on_deletemodels.CASCADE结果删一个分类连带把商品和订单明细全删了数据库直接被掏空。我在实际调试中吃过这个亏所以现在的建议是关联核心交易数据的外键用PROTECT保护起来。比如商品挂在分类之下你删分类前必须先处理掉分类下的商品否则数据库拒绝删除。会员字段用SET_NULL配合nullTrue这样删会员不会影响历史订单订单里保留的会员号变成空值而已。订单和明细之间的关系才用CASCADE一张订单删除它的明细一起清掉这是合理的。这个设计逻辑在答辩时可以直接回答“你用外键时考虑过删除影响吗”这类问题说明你不只是会用ORM建表而是考虑到了数据一致性。3. 数据可视化模块从后端聚合到图表渲染的完整落地3.1 为什么选择ECharts做图表而不是其他库市面上可选的图表库有几个Chart.js、ECharts、AntV G2还有D3.js。我在这个项目里选的是ECharts理由如下文档直观示例丰富。对毕设项目来说你不需要从零学习复杂的配置项对着官方示例改改就能出效果。中文文档和社区资料特别多遇到问题搜索一下就有解决方案这对赶进度很重要。默认的主题和交互效果相当能打。tooltip、图例切换、数据缩放这些功能开箱即用演示时在页面上滑来滑去非常加分。Chart.js也很轻量但做复杂仪表盘和多图表组合时ECharts的灵活度上限更高。D3.js太底层除非你有很强的JavaScript功底不然很容易被SVG操作和比例尺搞崩溃。3.2 后端数据聚合ORM的统计查询怎么写可视化看板的本质就是把数据库里的明细数据按维度聚合成统计结果再返回给前端。我用了一个dashboard应用来集中处理这些查询核心几个统计需求是这样的近7天销售额趋势本月各分类销售占比销售排行前10的商品库存预警商品列表今日订单数与实收金额近7天趋势的核心查询是这样写的from django.db.models.functions import TruncDate from django.db.models import Sum, Count today timezone.localtime() start_date today - timedelta(days6) trend_data ( SaleOrder.objects .filter(created_at__date__gtestart_date.date()) .annotate(dayTruncDate(created_at)) .values(day) .annotate( total_salesSum(actual_amount), order_countCount(id) ) .order_by(day) )有几个容易忽略的点第一created_at是DateTimeField直接按created_at分组会把同一天的记录散到不同小时里所以一定要先做TruncDate截断到天再分组。这一点我在初版实现时没注意导致图表X轴有几十个点排错排了很久。第二Django的ORM里时区设置会影响TruncDate的截断边界。使用USE_TZTrue的时候查询出来的“天”是按UTC0算的和我们本地时间差8个小时。解决方法是先timezone.localtime()取本地时间再基于本地时间做范围过滤和截断。我在项目里把settings里的TIME_ZONE设为Asia/ShanghaiUSE_TZ保持True查询时用localtime转换图表数据才是对的。第三要处理没有订单的日期。趋势图上如果某一天完全没有订单我们查出来的结果里就没有那天的记录图表上会直接少一个点。处理方式是拿到聚合结果后在Python里补全缺失日期把值补成0。这一步是为了视觉上的“连续感”答辩演示时你不会想让评委看到断档的折线。3.3 前端如何接数据并渲染图表后端聚合完成的数据通过JSON接口返回给页面。我的做法是设计一个简单的dashboard_view视图返回一个包含完整统计数据的context字典再由模板渲染到ECharts的option里。如果每次页面刷新都重新执行SQL查询开销不大毕竟毕设数据量小不用上缓存系统。但如果数据量大或者你希望演示时切换日期范围不刷新页面那接口方案更适合。接口方式是这样def sales_trend_api(request): days int(request.GET.get(days, 7)) # ...聚合查询... return JsonResponse({dates: dates, sales: sales, orders: order_counts})前端用fetch拉这个JSON再setOption渲染fetch(/dashboard/api/trend/?days7) .then(res res.json()) .then(data { chart.setOption({ xAxis: { data: data.dates }, series: [{ name: 销售额, data: data.sales }] }); });这里有一个前端的坑页面加载时div可能还没有拿到高度这时候初始化ECharts会让图表变0像素高。我的解决方法是把init放在window.onload回调里或者在div外层设定明确高度比如styleheight: 400px。这个看似无关紧要的问题实际调试时能把人急死我遇到过无数次“控制台没报错但页面空空如也”的情况最后定位到就是容器高度的问题。3.4 仪表盘的整体布局与展示建议看板页面我用了三行两列的整体布局第一行放销售趋势折线图和分类占比环形图第二行放TOP10商品横向柱状图和今日实时卡片第三行放库存预警表格和器周转简表。这样出来的页面信息密度高、层次感清晰演示时一屏扫过去就有“系统很完整”的观感。不要堆太多的图表。一个领导看板页面保留4到6张图足够了。每多一张图都要多写聚合查询、多写前端option还要多调试一条数据链路。信息过量反而会让答辩节奏变慢。4. 权限控制与销售业务闭环实现4.1 基于Django自带Auth扩展用户体系Django自带的User模型够用但有个小问题它没有“收银员”和“管理员”这种业务角色的直接字段。所以我在项目里用了自定义用户模型继承AbstractUser增加一个role字段class User(AbstractUser): ROLE_CHOICES ( (admin, 管理员), (cashier, 收银员), ) role models.CharField(max_length16, choicesROLE_CHOICES, defaultcashier)同时给不同页面做权限控制。比如商品进价和采购单只能管理员访问收银员只能看到收银台和订单查询。Django里最轻量的做法就是视图函数上加装饰器from django.contrib.auth.decorators import login_required, permission_required login_required permission_required(orders.can_refund, raise_exceptionTrue) def refund_view(request): ...这里需要注意一个细节自定义的权限不会自动生成你得在模型的Meta里定义好权限名称然后执行makemigrations让Django把它记录到数据库中否则装饰器会报权限不存在的错误。4.2 使用user_passes_test做角色页面隔离权限系统做细之后收银台就只允许收银员角色打开from django.contrib.auth.decorators import user_passes_test def is_cashier(user): return user.is_authenticated and user.role cashier user_passes_test(is_cashier, login_url/login/) def checkout_view(request): ...这种实现的好处是代码直观、答辩好讲。你只要说“我通过装饰器和role字段实现了基于角色的页面访问控制”评委就能理解。如果你愿意多花一点时间研究还可以用Django的Group Permission权限模型来管理页面访问但我觉得在毕设周期内Role字段方案更划算。4.3 收银下单与库存扣减的原子性收银模块是整个系统的业务核心也是最容易出错的地方。流程是这样的前端把购物车里的商品条目提交到后端后端校验库存扣减库存生成订单主表和明细表这一连串动作必须同时成功或者同时失败。这里最典型的坑就是不使用事务。比如你先把库存扣了然后生成订单时因为某个字段写错抛异常库存已经变了订单却没生成账就平不了。我在实现里用transaction.atomic把整段逻辑包起来from django.db import transaction transaction.atomic def create_sale_order(request): cart_items json.loads(request.POST.get(cart_items)) # 校验商品、计算金额... order SaleOrder.objects.create(...) for item in cart_items: product Product.objects.select_for_update().get(iditem[product_id]) if product.stock item[quantity]: raise ValueError(f商品{product.name}库存不足) SaleOrderItem.objects.create(...) product.stock - item[quantity] product.save() return JsonResponse({order_no: order.order_no})事务里用select_for_update()做什么这是对商品行加锁避免两个收银员同时卖同一个商品时超卖。简单说就是同一时刻只有一个请求能读取和修改这个商品的库存行另一个请求得排队。这个点在答辩里聊并发控制时是一个很扎实的技术亮点。还有一个细节一旦事务中途抛异常前面所有创建和修改都会被回滚包括库存扣减。所以前端收到错误提示时数据库是干净的不会出现积分扣了但订单没有的问题。4.4 退货流程与库存回补退货是反向操作流程反过来先验证订单存在然后把订单明细里的商品数量加回库存最后把订单状态标记为“已退货”。我在SaleOrder上增加了一个status字段取值有paid、refunded。退货时不实际删除订单而是置为refunded这样历史报表统计销售额时把已退货订单排除掉就行。直接删订单表面上省事但你会发现统计报表的数据越来越莫名其妙。5. 实操调试中的常见问题与排查技巧5.1 高频报错与解决办法表我把调试这个系统时碰到频率最高的几个问题整理成了表格你可以直接对照排查现象根本原因解决办法访问首页报TemplateDoesNotExisttemplates目录路径没配对在settings的TEMPLATES里配置DIRS为BASE_DIR / templates图表页只有坐标轴没有数据后端返回的JSON字段名和前端option对不上用浏览器的开发者工具查看接口实际返回内容登录后跳转回登录页未配置LOGIN_URL或Session过期确认settings里LOGIN_URL/login/并检查Cookie修改商品售价后历史订单价格变化订单明细没有保存价格快照按前面说的在SaleOrderItem里存price字段保存中文出现乱码数据库连接字符集不是utf8mb4在MySQL的connection配置加charsetutf8mb4admin后台登录不上自定义User模型后没有重新建库自定义了AUTH_USER_MODEL后必须重新migrate或者重建数据库按天统计聚合结果错位TruncDate受时区影响查询前调用timezone.localtime()模板中图片/静态文件404开发环境没有配置static路由在urls.py里用static()函数映射STATIC_URL5.2 图表不显示或者显示空白怎么办按我多次调这个模块的经验ECharts图表空白时优先按这个顺序排查先打开浏览器开发者工具的Console看有没有JS报错。最常见的是chart初始化时DOM节点还没渲染完成或者ECharts库未加载成功。再点开Network看JSON接口状态码。如果是404或者500问题在后端不是前端。拿到接口返回数据后确认数据结构是不是嵌套了一层。如果你用了JsonResponse({data: [...]})前端option的series.data里却直接用了data数组类型对不上图自然不出来。最后确认容器高度。只要div高度是0ECharts渲染出来就是0px高的空白必须在CSS里给容器设置固定高度。5.3 一个容易忽略的细节日期的格式化前端拿到后端返回的日期经常是2025-06-01T00:00:0008:00这种带时区偏移的ISO格式。直接用这个字符串当X轴坐标是可以的但显示出来很难看。我在聚合查询里返回时直接做了格式化trend_list [] for item in trend_data: trend_list.append({ date: item[day].strftime(%Y-%m-%d), total_sales: float(item[total_sales] or 0), })为什么要把Decimal转成float因为JsonResponse默认不能直接序列化DecimalField必须转成可以JSON序列化的类型。这个问题我第一次遇到时还以为是Django的Bug其实只是序列化的规则限制。5.4 并发测试时发现重复订单号订单号我最初用的方案是直接按时间戳拼接一个随机数并发访问下出现了两个订单号一样的情况。后来改成“日期自增序号”的生成方式根据当天已有订单数量生成一个序号拼在日期后面。虽然不保证绝对唯一但配合数据库的unique约束重复时会尝试重新生成就稳定多了。6. 从本地运行到部署演示再到答辩准备6.1 本地运行的关键步骤拿到源码后第一件事不是跑起来而是创建虚拟环境。我的建议步骤是python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install -r requirements.txt python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver有一个很多人没注意的点如果你在项目里已经自定义了AUTH_USER_MODEL第一步makemigrations之前一定要确认settings里的配置没错。如果顺序反了系统已经基于默认User模型生成了迁移文件和权限表再去改AUTH_USER_MODEL会提示你删除数据库或者从头migrate白白浪费半小时。6.2 演示环境的准备答辩演示时最怕现场翻车所以本地演示环境我建议准备一份固定数据商品分类至少5个每个分类下面5到10个商品条码数据要像真实超市那样成体系。订单数据准备近30天的每天10条左右这样趋势图看起来是连续的。库存预警至少准备3个低于阈值的商品方便演示预警功能。收银演示时准备一个预设商品的购物车列表现场不用临时想买什么。预先准备数据的做法是写一个management command或者简单的脚本一键生成测试数据。这样即使演示前把数据库删了也能用一行命令快速恢复到可用状态。6.3 答辩时评委最常问的几个技术点我把这类项目的问高频问题总结一下提前准备的话稳很多为什么要用Django而不用Flask答Django自带Admin、认证、ORM项目开发和维护成本低能在有限周期内做出完整系统。数据可视化模块的数据是怎么实时更新的答每次页面刷新重新聚合数据库如果数据量大可以加缓存毕设阶段数据量小直接查询最可靠。高并发下怎么防止库存超卖答事务加select_for_update行锁确保同一商品的库存扣减串行化。如果某天订单特别多图表怎么优化加载速度答聚合查询提前加索引或者把统计结果定时写入缓存表图表直接查缓存表。为什么订单表单独拆一个明细表而不是在订单里存JSON答明细表方便做单品销售排行、毛利核算、退货退单品等操作JSON存数据不好做数据库层面的统计。这些问题都不难关键是你真的理解自己写过的代码。如果某个点你是照抄别人的建议提前把源码读透别到答辩现场一问三不知。6.4 关于部署上线说实话毕业设计阶段不需要急着把系统部署到云服务器本地演示完全够用。但如果你想给简历上写一句“上线部署经验”可以考虑用一台云服务器跑一下pip install gunicorn gunicorn sales_system.wsgi:application --bind 0.0.0.0:8000再用Nginx反代一下把静态文件和媒体文件交给Nginx处理。这一步要注意先执行collectstatic收集Django的静态文件否则管理后台和图表页面样式全丢。本地开发时Django会自动处理静态文件一上服务器这套自动规则就不生效了这也是初学者最容易翻车的地方。个人实操体会这套系统从建模到跑通再到把可视化模块打磨得比较像样我前后调试了多次。最大的体会就是不要一上来就去敲代码先花一晚上把表结构和几个核心流程的数据流图画出来。表结构定了后面写代码就是“翻译”的过程顺很多。而可视化模块的正确打开方式是先把接口的数据聚合逻辑跑通用浏览器直接访问JSON地址确认数据正确了再去写前端图表。前端只是最后一步的“展示层”数据对了展示就是水到渠成的事情。还有一个细节想单独说。答辩现场的演示顺序建议先讲整体架构和数据流再打开看板页面展示图表最后用收银台实际走一遍下单-扣库存-生成订单的闭环整个演示过程就会形成一个完整的故事线。提前把收银台要用的商品准备好、把演示用的会员账号准备好会比现场临时操作要稳得多。这套项目如果吃透了不只是毕业设计的问题它几乎覆盖了一个Web业务系统从数据建模、权限控制、事务处理到数据可视化展示的完整链路写进简历里也是能直接拿出来讲的经历。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑