十多年前我刚开始写业务系统的时候,做一个超市管理系统的需求调研就能折腾一周:要先画业务流程图、设计表结构、还要考虑数据字典,最后写出来的代码还经常因为人员调整被推倒重来。后来用Django做类似的东西,明显轻松一个量级。今天想借着一个经典题目——django小型超市管理系统(附源码27379),聊聊在Python生态里做这类业务系统,从选型、建模、视图逻辑到上线排错,哪些地方值得你多看两眼,哪些坑你要是绕不过去,能白加两天班。
这个系统本身并不复杂,无非是商品管理、库存、销售记录、简单的统计报表这些模块。但越是这种“小系统”,越适合拿来磨技术:ORM建表、表单校验、会话登录、模板渲染全都能覆盖到。而且跟着源码一步步拆开看,比直接啃文档轻松得多。适合什么人?刚入门Django想找个完整项目练手的,准备做课程设计或者毕业设计的,还有想参考别人怎么组织中小型业务代码的,都能从里面找到能用上的东西。
1. 项目核心思路与Django选型逻辑
1.1 为什么这类系统首选Django而不是Flask
很多新人纠结的第一个问题就是:框架选Django还是Flask。我的观点很直接:如果项目有明确的业务对象(商品、订单、库存),要管理后台,要统计报表,短期内是一个人开发但未来可能交接给同事,那选Django一定不会后悔。
原因其实特别朴素。Django自带了Admin后台,你只要把数据模型定义好,一个能录入、能编辑商品和订单信息的后台就出来了。这对超市管理系统这种业务来说,是实打实的节省时间——你不需要为“商品信息的增删改查”单独写一堆视图函数,Django帮你把脚手架搭好了。哪怕后来你觉得自带的Admin样式不够看,想在它基础上叠加自定义页面,也不影响。
另一个原因是Django的ORM做多表关联查询非常顺手。超市系统里最常见的一种查询是:“找出这个月销量排前十的商品,并且按销量降序排列”。在Django里用annotate配合Sum就能搞定,几行代码。Flask配SQLAlchemy也能写,但模型的自动迁移、字段校验、迁移版本管理这些模块,需要你手动整合。Flask是让你自由组装,而Django是帮你把整套方案都摆好,你就往里填业务就够了。
在做“小型管理系统”这个场景下,“开箱即用”的收益往往要比“微框架带来的一点灵活性”更实在。尤其是开发周期被压缩到几天甚至一个周末的时候,你根本没有时间去评估Flask的组件选型。对新手来说更是如此:Django的约束会把你往规范的方向上带,Flask则需要自己给自己立规矩。
1.2 一个超市系统的核心需求到底拆成哪些模块
我接触到的很多课程设计、毕设题目,其实都长一个样:要求做一个能管商品、能管库存、能记录销售的后台。但很多同学拿到需求直接开始写代码,没有一个从需求倒推表结构的过程,后面写起来非常难受。真正动手前,我习惯把超市系统的核心业务拆成5个闭环:
第一是商品档案。商品不只是“名称”和“价格”两个字段这么简单。你得考虑商品编码、条形码、分类、单位(箱、瓶、袋)、进货价、零售价、库存下限。上限、下限这种字段虽然初期不起眼,但做库存预警的时候没它不行。
第二是入库管理。超市要进货,进货要有单子,单子里要记录进了哪些商品、数量、进货单价、供应商信息。入库之后,库存表里的数量要增加。
第三是日常销售。收银台扫码也好、手动填写也好,总之每一笔销出去的东西,都要形成销售记录,同时扣除库存。这里涉及事务处理——只要扣库存和生成记录不是原子的,早晚出问题。
第四是库存盘点和预警。账面库存和实际库存可能会不一致,所以需要盘点功能。预警则是针对库存低于下限的商品自动列出来,提醒采购补货。
第五是统计报表。老板肯定想知道今天卖了多少钱、哪些商品卖得好。这部分就是简单的销售汇总、利润计算,做几个带筛选条件的榜单。
你看,真正分下来其实就这么几块。源码27379这类项目通常就是按这个业务逻辑来组织的:goods模块管商品、stock模块管库存、sales模块管销售,再加一个dashboard做数据看板。理解了业务闭环,你再看别人的源码,就不会被路由和视图牵着鼻子走。
1.3 项目目录结构与分层思路
拿到一份Django源码,第一步不是打开models.py看热闹,而是先看目录结构。一份结构清晰的项目,通常长这个样子:
manage.py myshop/ # 项目配置目录 settings.py urls.py wsgi.py apps/ goods/ # 商品模块 models.py views.py urls.py admin.py sales/ # 销售模块 models.py views.py urls.py dashboard/ # 统计模块 views.py users/ # 用户与登录 models.py views.py templates/ # 全局模板 static/ # 静态资源 media/ # 用户上传图片等 utils/ # 公共函数这里有两个容易犯迷糊的点,我专门说一下。第一个是为什么把功能打包成独立app而不是全塞在一个app里。新手最常见的问题就是把所有models都写在一个文件里,短时间图省事,但项目超过三个模块后,改一个模型文件就要在几百行里找字段,而且应用间的边界消失,改一个模块容易误伤另一个模块。独立app的意思是让每个模块能自解释——goods模块里就是商品分类、商品信息;sales模块里就是销售单、销售明细。互相之间通过外键或者业务服务层来协作,而不是直接把对方模块的模型拿过来一顿乱用。
第二个是配置文件中自己写的核心在于数据库和app注册。settings.py里注册app的时候千万别少写,少写一个app,后面跑migrate的时候不会报错,但运行起来访问对应路由就会提示No module found,排查半天。这类问题一般出现在你复制别人项目之后,按自己的路径调整时漏了配置。
2. 数据建模:超市系统最关键的一层
2.1 商品、分类、库存三张表怎么设计才合理
很多下载来的源码里商品模型长得很随便,这是我最想吐槽的地方。一个合格的超市商品模型应该包含:商品编码、商品名称、条形码、所属分类、单位、规格、进货价、零售价、当前库存、库存预警阈值、创建时间、更新时间。类编码是唯一的,条形码也不该为空。有些实现直接把“当前库存”放在商品表里,这种做法不是不行,但如果你有入库流水、销售流水,库存就可以通过流水动态计算出来。可这么做的效率太低,所以常见方案还是维护一个冗余的库存字段,每次出入库事务里同步更新。
我建议你把分类单独做成一张表,不要用一个字符串字段把分类写死。这样做的好处是:后期要按分类统计销售额,可以直接按分类分组聚合,不用解析字符串、不用做条件判断。分类表里就三个字段:name、parent(自关联)、sort_order。超市商品分类最多两三级,自关联足够用,不需要太复杂。
库存这个地方容易忽略一个属性:库存扣减必须带条件更新。我来举个例子。销售一笔商品,不能光用goods.stock -= quantity这种写法,多线程并发或者管理后台并发操作时,容易出现超卖。Django里可以用F表达式来原子更新:
from django.db.models import F Goods.objects.filter(id=goods_id, stock__gte=quantity).update( stock=F('stock') - quantity )filter(stock__gte=quantity)保证库存不足时不更新,F('stock')保证读取和写入在同一条SQL里完成,不是取出值在Python里计算再存回去。这个写法很推荐你直接抄进项目里,既能防超卖,也省了锁。
2.2 销售订单和销售明细的拆分设计
这个环节新手特别容易做成一条销售记录里用逗号拼接商品名和数量。如果只是交作业也许能混过去,可项目一旦要算“某商品这个月卖了多少”,逗号拼接的数据会让你欲哭无泪。正确的做法是把订单头和订单明细拆成两张表:
订单主表(Order)保存这笔销售的整体信息——订单号、收银员、总金额、支付方式、下单时间。订单明细表(OrderItem)保存每一条商品级别的数据——关联哪个订单、哪个商品、下单数量、单价、小计金额。
为什么要拆?因为一张订单可以包含多个商品。如果只在订单表里存一个商品信息,就要为每个商品生成一张订单,那样想按订单维度统计客单价就很别扭,而且逻辑上也不正确。拆了明细表之后,想算某个商品的销量,直接在OrderItem表上聚合:
from django.db.models import Sum sale_stats = OrderItem.objects.filter( order__create_time__date=target_date ).values('goods__name').annotate( total_quantity=Sum('quantity') ).order_by('-total_quantity')[:10]values('goods__name')表示按商品名称分组,annotate做聚合,order_by取前十。三行代码拿到每日销售榜,视觉效果拉满,代码也干净。
两表让你明白了为什么要拆,我再提醒一个字段上的细节:金额到底存DecimalField还是FloatField。只要涉及钱,一律用DecimalField。Float是二进制浮点数,0.1加0.2会变成0.30000000000000004,做金额汇总时哪怕差一分钱,后面财务报表核对的时候能让你怀疑人生。DecimalField要指定max_digits和decimal_places,例如max_digits=10, decimal_places=2。Python的Decimal在转换时要注意:从字符串转,不要直接从Float转,否则精度损失又回来了。
2.3 外键关系的设计陷阱与on_delete选择
Django模型里的外键字段,最不起眼却又最致命的就是on_delete参数。很多早期源码喜欢写on_delete=models.CASCADE,意思是删掉关联的一方,另一方也会自动删掉。可是在超市系统里,你万万不能让删掉一个商品分类,就把分类下所有商品全删了;更不能因为删了一个供应商,把它的进货记录一起删了。
正确的思路是:凡是属于“历史记录”性质的表,外键一律用models.PROTECT或者SET_NULL。我来具体解释一下这两个的区别:
PROTECT:如果还有关联记录,删除操作会被禁止。比如你试着删除一个已经有进货记录的供应商,Django会抛ProtectedError,告诉你不能删。这在业务上就是正确行为——历史记录不该被连带删除。SET_NULL:删除关联对象后,该字段自动变成NULL。适用于“商品被删除后,销售明细中保留商品名称快照”这种设计。前提是字段定义时null=True, blank=True。
有一个实际业务场景我建议用第三种做法:不物理删除,加一个is_active布尔字段。比如商品下架,并不是从表里删除,而是设置is_active=False。这样历史销售明细里还能关联到商品,只是前台不再显示。这种做法比任何外键删除策略都安全,报表统计也不会断链。
3. 后端核心模块的实操实现
3.1 登录认证与session cookie踩坑经验
超市系统一般分为管理员和收银员两种角色,通常会做一个简单的用户登录。Django自带的django.contrib.auth模块非常方便,User表、密码加密、session管理全都是现成的。用的时候有一个重要动作:不能直接对Django自带的User表动手动脚。
如果要在用户模型上增加手机号或者角色字段,推荐的做法是新建一个Profile模型,和User建立OneToOneField关系,或者用get_user_model()配合自定义用户模型。不建议直接改Django源码里的User表,那会让你升级Django版本时痛不欲生。
登录视图我写过太多次了,核心逻辑无非是:
from django.contrib.auth import authenticate, login def user_login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('dashboard:index') else: messages.error(request, '用户名或密码错误') return render(request, 'login.html')第一版项目用这个写法就够了。需要警惕的是:authenticate()只负责验证,不会做登录,你还得主动调用login()把用户写入session。很多同学在这里漏了login,导致明明验证通过,页面就是不跳转。
session和cookie的问题,往往出现在前后端分离或者一个项目部署到多个域名时。Django的session默认存在数据库里,cookie里只存sessionid。后台设置SESSION_COOKIE_SAMESITE = 'Lax'比较稳妥,太严格会有些跨域请求不认登录态,太宽松又有安全风险。另外别忘了CSRF_COOKIE_SECURE和SESSION_COOKIE_SECURE在生产环境设成True,前提是网站跑在HTTPS下。
3.2 入库、销售、退换货的视图逻辑怎么组织
写这类有数据流转的视图,最重要的思维是“事务”。入库意味着库存变多,销售意味着库存变少,退换货意味着库存恢复或者库存再次减少,每一步都必须保证要么全部生效、要么全部回滚。Django里用transaction.atomic()包函数体就行:
from django.db import transaction @transaction.atomic def create_sale_order(request): # 校验参数 # 创建订单主表记录 # 创建订单明细 # 扣减库存 pass我见过不少新手在这个环节踩的坑是:创建订单成功了,扣库存的时候报错,导致订单数量对不上。加了transaction.atomic()之后,整个函数块里的任何一个异常都会让前面的数据库操作全部回滚,数据一致性才有保障。
视图函数的组织方式,我建议一个业务场景一个函数,不要搞一个万能函数。入库就是stock_in,销售就是create_sale_order,退货就是return_goods。参数校验放进Django Forms或者ModelForm里。虽然小项目里手写POST参数校验也不是不行,但用Forms能自动帮你处理字段类型转换、必填校验、错误信息整理,还自带渲染模板的能力,代码会干净很多。
另外建议所有POST请求的操作在完成后加一条messages.success()的提示,并且在模板里渲染出来。用户操作完很需要反馈,否则他以为没点上、反复点提交,后台就多出好几笔重复订单。
3.3 分页、搜索、统计的QuerySet常用写法
超市系统里最常出现的需求是“商品列表页要带搜索、要分页、要按分类筛选”。Django的QuerySet写起来非常丝滑。搜索通常用icontains做模糊匹配:
goods_list = Goods.objects.all() search_keyword = request.GET.get('keyword') category_id = request.GET.get('category') if search_keyword: goods_list = goods_list.filter(name__icontains=search_keyword) if category_id: goods_list = goods_list.filter(category_id=category_id)重点是:你可以放心地链式调用filter,因为QuerySet是惰性的,前面的filter不会真的执行数据库查询,只有在你遍历或者取值的时候才真正执行。所以大可不必像用原生SQL那样操心拼动态条件。
分页直接用Django内置的Paginator,这也是源码里最常见的写法:
from django.core.paginator import Paginator paginator = Paginator(goods_list, 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number)get_page()是兼容性很好的方法,页码超出范围或者非数字时不会抛异常,而是自动返回第一页或最后一页。模板里渲染上一页下一页时,用page_obj.has_previous和page_obj.has_next判断。这里有个容易被忽略的需求:翻页时保留搜索条件。模板里分页链接要带上?page=2&keyword={{ keyword }},不然翻页之后搜索条件丢光了。
统计的时候我前面提过要用annotate,我再补一个更完整的例子,统计每个商品分类下的销售额:
from django.db.models import Sum, F category_stats = OrderItem.objects.values('goods__category__name').annotate( total_sales=Sum(F('quantity') * F('price')) ).order_by('-total_sales')F('quantity') * F('price')是在数据库层面做乘法,不会把每一行取到Python里算,效率高,代码也简洁。这个写法在日报、周报、月度榜单场景里非常好用。
4. 前端页面与模板渲染思路
4.1 Django模板语言在小型项目里的实用姿势
不少同学一看到Django模板就嫌弃:不是用变量来只会if for吗?确实,Django模板语言的设计目标就是保持简单,不让写复杂业务逻辑。但把它的特性用对了,小项目的页面开发效率并不低。
我建议你在templates目录下做一个base.html,把页面骨架写出来——导航栏、内容区、底部脚本、消息提示区域都搁在这里。然后每个业务页面只需要{% extends "base.html" %},重写content块就好。这样做的好处是改样式、加菜单,只要是公共结构调整,一处改全站生效。
模板里加载静态文件记得第一行写{% load static %},否则{% static 'xxx' %}会报错。我第一次写这个就忘了load,浏览器里看样式全炸了,查了半天才发现不是CSS路径问题,是模板标签没加载。
模板里展示列表数据,常配合Bootstrap表格。表格行上的操作按钮,比如编辑、删除、入库、出库,可以用URL反向解析{% url 'goods:goods_edit' goods.id %}。使用反向解析的好处是URL结构将来调整,不需要改模板,只要路径配置正确,解析自动对应。
4.2 不用前端框架的情况下怎么做出现代感
如果你不想引Vue、React这种重前端工具,又想页面不那么土,我有一个实用性很强的方案:Bootstrap 5 + 少量原生JavaScript + Chart.js。
Bootstrap负责页面布局、表格、按钮、表单、模态框这些基础组件。登录页做成居中卡片、列表页做成圆角卡片表格、详情页做成两栏布局,一张中规中矩的管理后台脸就出来了。
Chart.js负责统计数据可视化,dashboard首页放今天销售额、订单量、销售趋势折线图、分类占比饼图,数据接口直接在Django视图里输出JSON,前端用fetch拉数据。
<canvas id="salesChart" width="400" height="200"></canvas> <script> fetch('/dashboard/sales-trend/') .then(response => response.json()) .then(data => { new Chart(document.getElementById('salesChart'), { type: 'line', data: { labels: data.labels, datasets: [{ label: '销售额', data: data.values, borderColor: 'rgba(54, 162, 235, 1)', }] } }); }); </script>视图返回JSON也很简单,用JsonResponse就行:
from django.http import JsonResponse from django.utils import timezone from datetime import timedelta def sales_trend(request): today = timezone.localdate() labels, values = [], [] for i in range(6, -1, -1): day = today - timedelta(days=i) total = Order.objects.filter(create_time__date=day).aggregate( s=Sum('total_amount') )['s'] or 0 labels.append(day.strftime('%m-%d')) values.append(float(total)) return JsonResponse({'labels': labels, 'values': values})模板页面的渲染和JSON数据传输不要混在一起。页面结构用Django模板渲染,数据动态更新用fetch拿JSON,前后端职责就清晰了。小系统这么干,维护成本很低。
5. 常见问题与排错实录
5.1 迁移数据库时常见的报错合辑
先列一个高频错误速查表,都是我自己动手时踩过或帮别人排查过的:
| 错误信息 | 常见原因 | 解决方案 |
|---|---|---|
No changes detected | 新加了模型或字段忘记生成迁移 | 运行python manage.py makemigrations后再migrate |
Field 'XXX' doesn't have a default | 给已有表添加非空字段没设默认值 | 给字段加default="xxx"或设null=True |
relation "xxx" does not exist | 数据库没同步表结构,或settings里连错库存 | 检查settings里的数据库配置,某个库的迁移顺序 |
django.db.migrations.exceptions.InconsistentMigrationHistory | 之前删过迁移文件导致迁移历史混乱 | 不要随意删除迁移文件,必要时保留执行记录重新梳理 |
初学者最爱犯的毛病就是把migrations目录整个删掉,以为能“重置”。这样只会让Django的迁移历史记录与数据库里的django_migrations表对不上,报出InconsistentMigrationHistory,非常难受。正确做法是:迁移文件只增不改,想要重置,把数据库drop掉新建,再重新执行migrate,千万别手贱删那个目录。
5.2 时区设置导致的统计偏移问题
Django的USE_TZ = True开启时,所有时间都以UTC存储,模板展示时需要转换到本地时区。常踩的坑是统计“今天的订单”,直接用create_time__date=datetime.now(),在UTC环境下晚8点之后的订单会被算到“明天”去。
正确的做法是用timezone.localdate()而不是date.today()。因为localdate()会自动按当前时区转换本地日期,无论服务器UTC时间是几点,算出来的都是用户的昨天、今天、明天。这块我专门在之前写的统计代码里体现过,换成localdate()之后所有筛选日期就都准了。
另外还有个细节:DateTimeField(auto_now_add=True)记录的写入时间是UTC时间,但如果存库、取数、展示三个环节用的时区不一致,前端就会显示和真实时间差8小时。解决办法是settings里配置好:
TIME_ZONE = 'Asia/Shanghai' USE_TZ = TrueUSE_TZ=True是指定“启用时区支持”,数据存储为UTC,展示时才按TIME_ZONE转换。TIME_ZONE='Asia/Shanghai'指定本地时区。两个都配好,基本不会出现差8小时的问题。
5.3 静态文件404与部署时的常见坑
开发环境里页面样式丢了,第一反应看控制台里静态文件是不是404。原因一般是settings里没有配STATICFILES_DIRS。你可能会遇到用STATIC_URL能看到路径,但找不到文件。需要检查两个地方:
STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static']模板里{% load static %}之后用{% static 'css/style.css' %},这样会拼成/static/css/style.css,Django开发服务器会自动找到你设置的STATICFILES_DIRS,静态文件就不会404了。
生产部署又是另一个故事。如果用Nginx部署,项目执行了collectstatic之后,Django自带的静态服务器是不会跑的,必须要让Nginx直接以/static/开头的内容映射到静态文件目录。后台Admin样式丢失,十有八九是STATIC_ROOT和Nginxalias路径不一致。具体部署时,把STATIC_ROOT指向一个明确目录,Nginx配置里alias指到同一目录,不要指到项目根目录导致路径重复。
还有一个很多人不知道的坑:Django的DEBUG=True时,静态文件服务由Django自己处理,但如果ALLOWED_HOSTS里没写你的访问域名,访问时会报DisallowedHost,整个页面会挂掉。部署时一定记得把生产域名写进去,不然连静态资源都不会正常加载。
6. 源码阅读与二次开发建议
6.1 拿到一套Django源码,先看什么后看什么
如果你是下载了一套别人写的超市管理系统源码,不要急着把代码往Pycharm里一扔就点Run。先按这个顺序扫一遍:
第一步看README。很多同学不看就直接跑,结果项目的人名、数据库密码都写死在配置文件里,完全跑不起来。先读文档能省不少时间。
第二步看settings.py。重点看INSTALLED_APPS、DATABASES、LANGUAGE_CODE、TIME_ZONE、STATIC_URL。尤其是数据库配置,拿到新机器上第一件事是把它改成你自己本地的数据库名、用户名、密码。INSTALLED_APPS里注册了哪些app,决定你后续运行哪些迁移、能访问哪些路由。
第三步看项目根URL文件urls.py,通过路由一览这个项目的功能模块。把路由和功能对应起来,你马上就能知道这个系统有哪些页面。
第四步看models。一份设计优秀的源码,模型之间关系清晰,字段命名规范。如果发现模型乱成一锅粥,直接劝退,没必要浪费时间踩坑。
6.2 想给系统扩展新功能时,动刀要讲规矩
拿到源码不是终点,很多人是想要二次开发的。我给一个实际建议:先确定扩展范围,再动代码。比如要新增“供应商管理”,先在脑子里过一遍:新表结构、跟现有商品模型的关联、需要几个页面、需不需要权限控制。确定之后,新功能放新app,不要往已有模块里塞。
如果要在现有模型上加字段,那就在模型类里加字段,然后makemigrations再migrate,不要手动改数据库表。越是用不熟悉的源码,越要顺着框架的规矩来做。为了快而破坏规则,后期维护成本一定加倍。
改页面样式倒是没有太多风险。直接在base.html里调整导航和布局,或者在static/css里覆盖Bootstrap样式就成。但提醒一点:不要在别人的模板文件里瞎加{% load %},有些模板继承链很长,改动一个块可能影响多个页面。稳妥做法是改动范围内的小组件,而不是大面积改公共模板。
用这套经验,再回头看Django小型超市系统
我前前后后接触过几十个类似的项目源码,Django小型超市管理系统这类题目,最大的价值不在于系统本身有多复杂,而在于你通过打通一个完整业务闭环,真正理解了“Web框架怎么解决现实问题”。
看这类源码时,我最建议你做一件事:不要只看代码,先看数据模型。把模型字段和业务场景对应上,然后顺着一笔订单从创建到库存扣减的路径走一遍,手动在页面上操作,接着跟着代码断点一步步查,你会发现原来书本上讲的外键、事务、QuerySet,活生生出现在眼前。
我个人很喜欢的做法是拿别人的源码改造成自己的风格。比如加一个供应商管理,把商品Excel导入导出的功能补上,或者把报表页改成按小时统计客流。这些看起来不起眼的改动,能让你把框架能力真正变成自己的工具。踩过几次坑之后,后来再做类似的项目,连“抄”都不用抄,随手就能搭一个出来。这就是源码项目的意义——它不是让你复制粘贴交差,而是给你一条可以少走弯路的起点,剩下的路,得靠你自己去踩出来。