简介:基于Python Django开发的商店购物系统是一项完整的毕业设计级项目,面向计算机相关专业学生、Django初学者以及需要快速搭建电商原型的技术人员。项目针对传统电商系统中产品数据库模型难以适配多样化商品变体的问题,采用贴合物理属性的建模方式,有效避免实体属性值模型带来的大量表联接和性能损耗,同时保留了深入的商品层级结构。压缩包为rar格式,体积仅2.19MB,共427个文件,涵盖166个py后端逻辑、101个html模板、44个rst文档、29个png图片以及js、scss、po等多语言资源,目录划分清晰,便于按模块阅读和二次开发。整个项目可直接运行,适合作为毕业设计演示、课程设计参考或电商系统扩展起点。目前已有191人学习下载,资源完整度较高,能帮助使用者快速理解Django的模型设计、视图交互与前端模板组织方式。
1. 这类毕设项目到底在考核什么
把"基于python Django的商店购物系统"拿来当毕业设计,老师真正想看的东西并不是你写了几万行代码,而是你有没有把Web开发里最核心的几条主线捋顺:从前端的商品展示,到后端的购物车、订单、库存扣减,再到管理员的后台数据维护,这条链路是不是完整且自洽的。很多同学一上来就急着写功能,结果把Django当成了单纯的模板渲染工具,代码是能跑,但一被问到"订单状态怎么流转""库存超卖怎么办""session和cookie谁在维护登录态"就答不上来。
这套系统的正确打开方式,是把Django当武器而不是当拐杖。Django自带的ORM、Admin后台、表单校验、会话管理、信号机制,每一个都能对应到购物系统里的具体业务模块——商品用Model定义,购物车用Session存储,下单用事务保证一致性,权限用Django自带的用户系统扩展。全文围绕"完整代码可直接运行"这个核心诉求,把从建项目到跑通下单全流程的细节拆开讲透,同时点出答辩时老师爱问的几个深水区问题。如果你是正在做这个选题的学生,或者想快速上手Django做Web项目的开发者,这篇文章可以当一份带排错提示的实战地图来用。
2. Django商店购物系统的技术边界与选型依据
2.1 为什么是Django而不是Flask或FastAPI
做购物系统这类业务密集型项目,选型的第一标准不是性能,而是"内置能力能不能覆盖常见业务模块"。Django官方自带的Admin后台可以直接当商店的管理端用,商品表、订单表、用户表注册进去就有增删改查界面;ORM让你不用写原生SQL就能完成多表关联查询;表单系统自带CSRF防护和数据校验。这些特性拼在一起,意味着你不需要为了一个毕业设计再引入额外的第三方库去做基础功能,代码量少,出错的面也小。
对比一下另外两个框架就能看出来差别:Flask灵活度高但一切靠自己搭,ORM要选SQLAlchemy,Admin要配Flask-Admin,登录要处理Flask-Login,项目结构也得自己设计——这对毕设来说"自由"反而是负担;FastAPI性能好、适合前后端分离,但毕设通常要求的是一个完整的、能演示的站点,FastAPI默认不带模板体系和Admin模块,上手成本更高。
2.2 MVT架构在购物系统里的落点
Django采用MVT(Model-View-Template)架构,这个"T"是模板而不是控制器,URL路由承担了控制器的作用。映射到商店购物系统里,Model对应的是商品(Product)、分类(Category)、订单(Order)、购物车项(CartItem)这些数据表;View是一个个业务处理函数或类视图,比如"把商品加入购物车""提交订单""修改库存";Template是前端页面,负责把视图传来的数据显示出来。
理解这个架构的关键在于明白数据是怎么流动的。用户点击"加入购物车",浏览器发起一个POST请求,URL路由根据正则或路径转换器把请求分发给对应的视图函数,视图函数操作Model完成数据读写,最后返回一个HttpResponse或渲染后的模板。数据在Model、View、Template之间单向流转,不会出现业务逻辑散落在前端脚本里的情况——这是答辩时讲系统架构最清楚的表达方式。
2.3 Python版本与虚拟环境的选择
Django对Python版本有明确的兼容矩阵,选错了会在安装阶段就卡住。目前Django主流的3.2 LTS和4.x系列都要求Python 3.8以上,建议直接装Python 3.10或3.11的稳定版,避开3.13这种刚发布不久、第三方库适配还不全的版本。
项目依赖一定要装在虚拟环境里,这是"完整代码可直接运行"的前提。常见的做法是用Python自带的venv,不需要额外安装virtualenv:
# Windows python -m venv venv venv\Scripts\activate # macOS / Linux python3 -m venv venv source venv/bin/activate激活后命令行提示符前面会出现(venv)前缀,表示当前已经处于虚拟环境。然后再安装Django:
pip install django pip list安装完成后用pip list确认版本。很多同学直接pip install django装到了全局环境,换一台电脑或换一个项目就各种依赖冲突,管理起来很被动。把依赖固化到requirements.txt里是基本习惯:
pip freeze > requirements.txt换环境时执行pip install -r requirements.txt就能还原,这也是"可直接运行"最基础的一层保障。
3. 用Django搭建商店系统的最小可运行骨架
3.1 创建项目和应用的标准命令序列
Django项目和应用是两个概念,项目是整体配置,应用是业务模块。商店购物系统建议拆成两个应用:products管商品和分类,orders管购物车和订单,用户直接用Django自带的auth应用。这样做的好处是业务边界分明,答辩时讲"系统分为商品模块、订单模块、用户模块"也更有说服力。
# 创建项目,注意最后的点号表示在当前目录生成,避免多套一层目录 django-admin startproject shop . # 创建两个业务应用 python manage.py startapp products python manage.py startapp orders把新应用注册到配置里才能用,打开shop/settings.py找到INSTALLED_APPS列表,加上products和orders。同时顺手做两件事:把LANGUAGE_CODE改成'zh-hans',TIME_ZONE改成'Asia/Shanghai',这样Admin后台和模板里的日期显示才是中文。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'products', # 商品应用 'orders', # 订单应用 ] LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai'LANGUAGE_CODE影响Django内置文案的翻译语言,TIME_ZONE影响datetime.now()返回的时间,只改TIME_ZONE不改USE_TZ会导致时间存储和展示相差8小时,这个问题在订单创建时间上特别明显。
3.2 商品模型设计与Admin注册
商品模型是系统的地基,字段设计直接决定后续所有功能的开发难度。一个最小可用的商品模型应该包含名称、价格、库存、描述、图片、上下架状态这几个核心字段,分类单独建表用外键关联。
在products/models.py里写入:
from django.db import models class Category(models.Model): name = models.CharField(max_length=50, verbose_name='分类名称') sort_order = models.IntegerField(default=0, verbose_name='排序权重') class Meta: verbose_name = '商品分类' verbose_name_plural = verbose_name ordering = ['sort_order'] def __str__(self): return self.name class Product(models.Model): category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name='products', verbose_name='所属分类' ) name = models.CharField(max_length=200, verbose_name='商品名称') price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='售价') stock = models.PositiveIntegerField(default=0, verbose_name='库存') description = models.TextField(blank=True, verbose_name='商品描述') is_active = models.BooleanField(default=True, verbose_name='是否上架') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '商品' verbose_name_plural = verbose_name ordering = ['-created_at'] def __str__(self): return self.name def reduce_stock(self, quantity): """扣减库存,库存不足时抛异常""" if quantity > self.stock: raise ValueError('库存不足') self.stock -= quantity self.save(update_fields=['stock'])字段类型的选择有讲究:价格为什么用DecimalField而不是FloatField,因为浮点数在计算机里是二进制近似存储,0.1加0.2算出来是0.30000000000000004,放到金额上是不能容忍的精度错误;库存用PositiveIntegerField保证不能为负;on_delete=models.PROTECT表示有商品关联的分类不允许删除,防止删了分类导致商品成为孤儿数据。
模型定义好之后要生成迁移文件并同步到数据库:
python manage.py makemigrations python manage.py migratemakemigrations根据模型变化生成迁移脚本,migrate把迁移脚本真正应用到数据库。这两个命令是Django开发里最频繁的操作,模块新增字段、修改字段类型后都要重新执行一遍。
接着在products/admin.py里注册模型,让商品能通过后台管理:
from django.contrib import admin from .models import Category, Product @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ['name', 'sort_order'] @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ['name', 'category', 'price', 'stock', 'is_active'] list_filter = ['category', 'is_active'] search_fields = ['name']list_display控制后台列表页显示哪些列,list_filter在右侧生成筛选器,search_fields开启搜索框。这一步做完,创建管理员账号登录后台就能看到商品管理界面了:
python manage.py createsuperuser python manage.py runserver3.3 商品列表与详情页的模板渲染
前端页面用Django模板语法渲染,视图从数据库取数据传参到模板。在products/views.py里写两个视图函数:
from django.shortcuts import render, get_object_or_404 from .models import Product def product_list(request): products = Product.objects.filter(is_active=True) return render(request, 'products/list.html', {'products': products}) def product_detail(request, product_id): product = get_object_or_404(Product, id=product_id, is_active=True) return render(request, 'products/detail.html', {'product': product})get_object_or_404在商品不存在时自动抛404异常,省去了手动判断加try/except的步骤。配置好URL路由才能访问到这两个视图,在shop/urls.py里做一次总路由分发:
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('products.urls')), path('orders/', include('orders.urls')), ]然后products/urls.py里写:
from django.urls import path from . import views urlpatterns = [ path('', views.product_list, name='product_list'), path('product/<int:product_id>/', views.product_detail, name='product_detail'), ]模板文件放在products/templates/products/目录下,这个路径约定是Django的app目录模板加载规则。列表页的核心代码是模板循环:
{% for product in products %} <div class="card"> <h3>{{ product.name }}</h3> <p>{{ product.price }} 元</p> <a href="{% url 'product_detail' product.id %}">查看详情</a> </div> {% empty %} <p>暂未上架任何商品</p> {% endfor %}{% url %}标签通过路由的name反解析出真实URL,好处是路由地址调整后模板不需要改。程序运行时,Django模板引擎先把{{ product.name }}替换成对应字段值,再把整个页面作为字符串返回给浏览器。
4. 购物车、下单与库存扣减的核心实现
4.1 购物车用Session存储还是数据库存储
购物车的实现方案有两种:一种是把购物车数据存在Session里,另一种是建CartItem表用数据库存。毕设场景下推荐Session方案,因为匿名用户也可以使用购物车,不需要强制登录;而数据库方案需要处理游客身份的识别,复杂度高很多。
Session方案的原理是:Django为每个访问者生成一个随机session key,以cookie形式写回浏览器,服务端用这个key在django_session表里找对应数据。购物车数据以字典形式存进session,结构是"商品ID->数量"的映射:
def cart_add(request, product_id): quantity = int(request.POST.get('quantity', 1)) cart = request.session.get('cart', {}) product = get_object_or_404(Product, id=product_id) if not product.is_active: return redirect('product_list') cart[str(product_id)] = cart.get(str(product_id), 0) + quantity request.session['cart'] = cart return redirect('cart_detail')注意cart字典的key必须转成字符串。JSON序列化时字典的key如果不是字符串会出问题,Django的Session框架底层用的是JSON序列化,而JSON规范的key必须是有序字符串,这一点特别容易踩坑:整数key能存进去,但取出来的时候可能已经变成了字符串,判断逻辑就会出错。
4.2 购物车页面的数量修改与商品总价计算
购物车页面要展示当前购物车里的所有商品,最基本的操作是遍历session里的商品ID,逐个查库拿到商品信息,再累加总价:
def cart_detail(request): cart = request.session.get('cart', {}) items = [] total_amount = 0 for product_id, quantity in cart.items(): try: product = Product.objects.get(id=int(product_id)) except Product.DoesNotExist: continue subtotal = product.price * quantity total_amount += subtotal items.append({ 'product': product, 'quantity': quantity, 'subtotal': subtotal, }) return render(request, 'orders/cart_detail.html', { 'items': items, 'total_amount': total_amount, })这段代码里有一个try/except处理商品被下架或删除的情况——购物车里的商品ID在session里不一定还对应有效的商品记录,continue跳过而不是报错,页面才能正常渲染。商品总价的计算放在视图层而不是模板层,好处是价格逻辑统一在Python里处理,模板只负责显示。
购物车数量的增减操作也走视图,比如修改数量:
def cart_update(request, product_id): cart = request.session.get('cart', {}) action = request.POST.get('action') if action == 'increase': cart[str(product_id)] = cart.get(str(product_id), 0) + 1 elif action == 'decrease': cart[str(product_id)] = cart.get(str(product_id), 0) - 1 if cart[str(product_id)] <= 0: cart.pop(str(product_id), None) request.session['cart'] = cart return redirect('cart_detail')这里用request.POST.get('action')区分加号和减号,比在URL里传参数更规范,避免GET请求修改服务端状态的副作用。
4.3 下单时用事务锁住库存
下单是整个系统里最需要严谨对待的环节。用户提交订单时,系统要做这几件事:创建订单主表记录、把购物车商品写入订单明细表、扣减商品库存、清空购物车。这四个操作只要有一个失败,其他三个也要回滚,否则会出现"订单建了但库存没扣""库存扣了但订单没建成"的数据不一致问题。Django的transaction.atomic()就是干这个的:
from django.db import transaction from django.utils import timezone def order_create(request): if request.method == 'POST': cart = request.session.get('cart', {}) if not cart: return redirect('cart_detail') # 收件信息从表单获取 receiver = request.POST.get('receiver') address = request.POST.get('address') phone = request.POST.get('phone') try: with transaction.atomic(): order = Order.objects.create( user=request.user if request.user.is_authenticated else None, receiver=receiver, address=address, phone=phone, total_amount=0, ) order_lines = [] total = 0 for product_id, quantity in cart.items(): product = Product.objects.select_for_update().get(id=int(product_id)) if product.stock < quantity: raise ValueError(f'{product.name} 库存不足') product.reduce_stock(quantity) line_total = product.price * quantity total += line_total order_lines.append(OrderItem( order=order, product=product, price=product.price, quantity=quantity, )) order.total_amount = total order.save(update_fields=['total_amount']) OrderItem.objects.bulk_create(order_lines) except ValueError as e: return render(request, 'orders/error.html', {'error': str(e)}) # 下单成功,清空购物车 request.session['cart'] = {} return render(request, 'orders/success.html', {'order': order}) return render(request, 'orders/checkout.html')这段代码里最关键的是select_for_update(),它在数据库层面对商品行加了行级锁。两个用户同时购买同一个商品时,第一个事务拿到锁,第二个事务必须等第一个提交后才能读取,这个机制从根源上防止了"超卖"——库存剩1件,两个用户同时下单,如果没有锁,两个事务都读到库存为1,各自扣一回,库存就变成负数了。
bulk_create一次批量写入所有订单明细,比循环里逐条create少发很多次SQL请求,性能更好。update_fields=['total_amount']只更新指定字段,避免Django把所有字段都重写一遍。
4.4 订单删除与关联数据处理
Django的执行查询与删除操作有一个常见的误区:直接delete()一个主表对象时,外键关联的子表数据怎么处理取决于on_delete参数。对订单和订单明细这对关系,推荐用CASCADE,删订单就自动删明细——这符合业务直觉:
class Order(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='下单用户' ) receiver = models.CharField(max_length=50) address = models.CharField(max_length=200) phone = models.CharField(max_length=20) total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) status = models.CharField( max_length=20, choices=[ ('pending', '待付款'), ('paid', '已付款'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ], default='pending' ) created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.PROTECT) price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField(default=1)on_delete=models.PROTECT用在订单明细关联商品上,含义是"被订单引用过的商品不允许删除",保留商品历史数据。在后台删除订单时,Django会自动级联删除所有明细;但如果你用的是QuerySet.delete()批量删除,注意它不会调用Model里重写的delete()方法,所以涉及自定义清理逻辑时要在post_delete信号里处理。
5. 毕设交付前的验证清单与答辩应急技巧
5.1 用Shell脚本自动验证核心功能
"代码完整可直接运行"不是嘴上说说,要在答辩演示前做一次全链路验证。把下面这段脚本存成check_project.sh,在项目根目录执行:
#!/bin/bash cd "$(dirname "$0")" echo "1. 检查Python环境" python --version echo "2. 检查依赖是否齐全" pip install -r requirements.txt -q echo "3. 检查数据库迁移状态" python manage.py makemigrations --check python manage.py migrate --noinput echo "4. 启动服务器测试首页" python manage.py runserver 8000 & SERVER_PID=$! sleep 3 curl -s -o /dev/null -w "首页状态码: %{http_code}\n" http://127.0.0.1:8000/ curl -s -o /dev/null -w "后台状态码: %{http_code}\n" http://127.0.0.1:8000/admin/ kill $SERVER_PID echo "验证完成"这段脚本做的事是从零检查一遍项目的可运行性:依赖有没有装、迁移有没有落后、两个关键页面能不能返回200状态码。答辩前跑一次,能避免现场"诶我本地明明能跑"的尴尬。
5.2 生成演示数据的方法
刚迁移完的空数据库没有商品,演示时页面空空如也不好看。在products/management/commands/seed_data.py里写一个自定义命令:
from django.core.management.base import BaseCommand from products.models import Category, Product class Command(BaseCommand): help = '生成演示用商品数据' def handle(self, *args, **kwargs): category = Category.objects.create(name='手机数码', sort_order=1) Product.objects.create( category=category, name='旗舰手机', price=5999.00, stock=100, description='演示用商品' ) self.stdout.write(self.style.SUCCESS('演示数据创建成功'))执行python manage.py seed_data即可。自定义管理命令是Django被忽略的实用功能,适合做数据初始化和批量操作,答辩时展示这一步能给老师留下"工程化习惯比较好"的印象。
5.3 Admin后台界面美化的最小改造
答辩前给Admin换肤是最低成本的美化方案,安装django-admin-interface包可以替换默认的Django Admin样式:
pip install django-admin-interface然后在INSTALLED_APPS里把admin_interface放到django.contrib.admin前面,加上colorfield,迁移后进入Admin后台就能在右上角看到"界面设置"入口,换主题色、折叠侧边栏、调整logo都可以可视化操作。这个改动对功能没有任何影响,纯视觉提升,适合在演示前花五分钟做完。
5.4 被追问时的三个深水区问题应答
老师问你"如何防止库存超卖",可以这么答:在事务里用select_for_update()锁住涉及的商品行,保证同一时间只有一个请求能扣减同一件商品的库存;再配合stock >= quantity的判断兜底,stock字段用PositiveIntegerField保证不可能为负。如果能再补充一句"生产环境下还可以加Redis分布式锁,或者用乐观锁CAS方案"就是加分项。
老师问你"Session里的购物车数据什么时候清掉",可以这么答:购物车保存在Session里,默认有效期两周,用户下单成功后主动清空;如果用户中途关闭浏览器,Session还在,下次打开还在;要主动清理可以用request.session.flush()销毁整个会话。这个细节能体现你理解"状态存储"的本质。
最后提醒一个重要细节:提交前删除项目目录下的__pycache__文件夹,压缩包体积会小很多。.gitignore里加上venv/、*.pyc、__pycache__/、db.sqlite3,如果用的是SQLite数据库,演示数据要不要提交这个问题提前想清楚,别让老师拿到压缩包双击运行的时候带着你的旧购物车数据上台。
本文还有配套的精品资源,点击获取