资讯动态

Django电商项目源码拆解:从模型设计到购物车订单实战

发布时间:2026/10/28 7:16:58 来源:尧图企业网站定制
简介这份基于Django与Python开发的在线购物平台源码面向正在完成课程设计或实战项目的计算机专业学生、Web开发入门者可帮助理解电商系统从用户认证到订单支付的整体实现链路。项目整合注册、登录、邮箱/手机号/第三方QQ/微信等多种登录方式并涵盖商品展示、购物车操作、订单生成、评论与搜索、支付宝在线支付等核心模块后台管理可维护商品、库存及订单状态适合作为毕业设计或课设答辩的基础框架。压缩包大小1.82MB当前文件总数未标注内容以Django项目源码、模板页面、配置文件及相关静态资源为主结构清晰便于对照学习。已有117人浏览/下载说明其对于同方向学习者具有一定参考价值。通过阅读源码可以掌握Django ORM建模、用户认证与权限控制、模板渲染购物流程等关键技法并可直接在其基础上二次开发、替换界面或扩展业务模块。1. 这份基于 Django 实现在线购物平台的 zip 源码到底值不值得花时间读先说我拆完的感受这不像网上那些只有 CRUD 的 Demo而是一个能把课程设计直接交上去的完整电商工程。商品、购物车、订单、后台管理、模板页面全都有解压 zip 后按文档把数据库配置改一下本地就能把项目跑起来。对正在做 python 课程设计、又不想从零搭框架的人来说这份源码的参考价值在于「完整的目录分层 真实业务表结构设计」而不是简单的代码搬运。适合对象很明确一是 Django 项目实战新手想看看一个合格的电商项目怎么拆模块二是时间紧需要一份能跑通、能讲清楚设计思路的源码做底子。它的核心链路是「浏览商品 → 加入购物车 → 下订单 → 后台处理」这条链路走通了Django 的 MVT 体系也就基本吃透了。2. 拆解项目结构一个 Django 电商平台为什么要拆成四个应用2.1 项目与应用的分工先理解 django-admin startproject 和 startapp 的区别拿到源码第一步不是急着跑而是先看目录。这个工程在根目录下分了shop、cart、orders、users四个应用模块外加一个存放全局配置的项目主目录。很多新手在这里容易混淆startproject生成的是「项目容器」管的是 settings、urls、wsgi 这些全局配置startapp生成的才是「业务应用」管的是具体业务逻辑。这份源码把用户、商品、购物车、订单拆成四个独立应用就是遵循 Django 官方推荐的分层思路——应用之间通过 URL 路由和模型关联协作而不是把所有视图堆在同一个views.py里。# 常见的创建顺序和这份源码的目录结构是一致的 django-admin startproject myshop cd myshop python manage.py startapp shop python manage.py startapp cart python manage.py startapp orders python manage.py startapp users创建完成后下一步是在myshop/settings.py的INSTALLED_APPS里注册这四个应用注意apps.py里的配置类名要对应上。如果漏了注册migrate 时模型表不会生成运行时会直接报OperationalError: no such table这类错误这是新手最常见的起步翻车点。我一般会先看INSTALLED_APPS列表和实际应用目录是否一一对应再往下读代码。2.2 核心数据模型Product、Cart、Order 的字段设计与关联关系这份源码里最能体现设计功力的地方是数据模型。我拆完models.py后发现它没有把字段一股脑堆在一个表里而是按电商业务拆成了五张核心表Category商品分类、Product商品、CartItem购物车条目、Order订单、OrderItem订单明细。其中Product和Category是外键关联CartItem与Product是多对一Order与OrderItem是一对多。这种设计符合第三范式的基本要求也方便后续做库存统计和订单报表。# shop/models.py 核心模型字段做了简化但结构一致 from django.db import models from django.urls import reverse class Category(models.Model): name models.CharField(max_length100, verbose_name分类名) slug models.SlugField(max_length120, uniqueTrue) class Meta: verbose_name 商品分类 verbose_name_plural 商品分类 def __str__(self): return self.name class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.CASCADE, related_nameproducts) name models.CharField(max_length200, verbose_name商品名称) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) stock models.PositiveIntegerField(default0, verbose_name库存) description models.TextField(blankTrue, verbose_name商品描述) image models.ImageField(upload_toproducts/%Y/%m/, blankTrue) is_active models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] indexes [models.Index(fields[id, slug])] def __str__(self): return self.name def get_absolute_url(self): return reverse(shop:product_detail, args[self.slug])参数说明DecimalField比FloatField更适合存价格避免浮点精度问题on_deletemodels.CASCADE表示分类被删时该分类下商品一并删除这在课程设计场景里没问题但生产环境一般改成PROTECT或SET_NULLrelated_nameproducts让category.products.all()可以直接反向查询商品列表。Meta里的indexes是数据库索引进阶用法商品按slug做详情页跳转时能明显加快查询速度。2.3 读懂订单与订单明细的状态机设计订单模块是这份源码里最值得看的业务逻辑。Order表里有status字段常见取值是pending、paid、shipped、completed四态OrderItem则记录了订单对应的商品快照包括下单时的商品名和单价。这样设计的原因是商品价格和名称可能后期被修改而订单是历史数据不能跟着变所以下单时要把关键信息冗余一份到OrderItem里。# orders/models.py class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (completed, 已完成), ] user models.ForeignKey(users.User, on_deletemodels.CASCADE, related_nameorders) total_amount models.DecimalField(max_digits10, decimal_places2, default0) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue) def calculate_total(self): total sum(item.subtotal() for item in self.items.all()) self.total_amount total self.save(update_fields[total_amount]) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) product models.ForeignKey(shop.Product, on_deletemodels.SET_NULL, nullTrue) product_name models.CharField(max_length200) price models.DecimalField(max_digits10, decimal_places2) quantity models.PositiveIntegerField(default1) def subtotal(self): return self.price * self.quantity这里要注意OrderItem.product的外键用了on_deletemodels.SET_NULL配合nullTrue意思是商品后来被删了订单明细依然保留只是product变成空。而product_name和price是冗余字段和product表的内容解耦。这是电商订单表的标准做法也最能体现设计者对数据生命周期的理解面试或答辩时讲这个点会很加分。3. 核心功能落地商品展示、购物车与订单事务处理3.1 商品列表与详情页用 Django 类视图减少重复代码这个项目的视图层用的是基于类的通用视图和函数视图相比代码量更少而且内置了分页和表单绑定能力。商品列表页用了ListView详情页用了DetailView分别对应商城最常见的两个页面场景。# shop/views.py from django.views.generic import ListView, DetailView from .models import Product, Category class ProductListView(ListView): model Product template_name shop/product_list.html context_object_name products paginate_by 12 def get_queryset(self): queryset Product.objects.filter(is_activeTrue) # 支持按分类 slug 过滤如 /shop/category/electronics/ category_slug self.kwargs.get(category_slug) if category_slug: queryset queryset.filter(category__slugcategory_slug) return queryset def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) context[categories] Category.objects.all() return context class ProductDetailView(DetailView): model Product template_name shop/product_detail.html context_object_name product slug_url_kwarg slug参数说明paginate_by 12让列表页自动分页每页 12 个商品模板里直接使用page_obj即可渲染上一页下一页链接get_queryset里用category__slug双下划线过滤这是 Django ORM 跨表查询的常用写法context_object_name定义了模板中对象变量的名字不设置的话默认是object_list和object。详情页通过slug_url_kwarg声明 URL 中参数名是slug与Product.slug字段对照这样商品链接既美观又利于搜索引擎。3.2 购物车实现session 方案和数据库方案怎么选这是我拆这份源码时最关注的部分。购物车有两种主流实现纯 session 存储和数据库持久化。这套源码两种都覆盖了——临时购物车用session但确认下单时会把数据同步写入数据库的CartItem表。这种折中设计的优点很明显游客加购不需要登录数据存在服务端session里安全性和可控性比 cookie 好下单时再把选中的商品固化到数据库保证订单与购物车的一致性。# cart/views.py 购物车核心逻辑简化版 from django.shortcuts import get_object_or_404, redirect from django.views.decorators.http import require_POST from shop.models import Product require_POST def cart_add(request, product_id): product get_object_or_404(Product, idproduct_id, is_activeTrue) quantity int(request.POST.get(quantity, 1)) cart request.session.get(cart, {}) # session 里以商品 id 为 key 存数量和价格快照 if str(product.id) in cart: cart[str(product.id)][quantity] quantity else: cart[str(product.id)] { quantity: quantity, price: str(product.price), name: product.name, } request.session[cart] cart request.session.modified True return redirect(cart:cart_detail)这段代码里最关键的是request.session.modified True。如果不显式设置当cart字典内容变化但引用未变时Django 可能认为 session 没有修改导致刷新后购物车数据丢失这是 session 方案的经典坑。require_POST装饰器确保加购操作只能通过 POST 提交避免被 URL 直接触发。session 里存price字符串而不是直接引用商品对象是为了在购物车页面不每次都查商品表时也能展示金额属于空间换时间的取舍。3.3 订单创建与库存扣减事务与并发控制下单这步是电商系统的关键路径涉及订单表写入、订单明细写入、库存扣减三件事。这套源码在orders/views.py里用 Django 的transaction.atomic()把这几个操作包进同一个数据库事务保证要么全部成功要么全部回滚。# orders/views.py from django.db import transaction from django.shortcuts import render, redirect from .models import Order, OrderItem def order_create(request): cart request.session.get(cart, {}) if not cart: return redirect(cart:cart_detail) if request.method POST: with transaction.atomic(): order Order.objects.create(userrequest.user) for product_id, item in cart.items(): product Product.objects.select_for_update().get(idproduct_id) if product.stock item[quantity]: raise ValueError(f{product.name} 库存不足) product.stock - item[quantity] product.save(update_fields[stock]) OrderItem.objects.create( orderorder, productproduct, product_nameproduct.name, priceproduct.price, quantityitem[quantity], ) order.calculate_total() request.session[cart] {} request.session.modified True return redirect(orders:order_detail, order.id) return render(request, orders/order_create.html)逻辑说明select_for_update()是行级锁会在数据库层面锁定这条商品记录直到事务结束。这是处理并发下单时防止超卖的关键手段——两个用户同时买最后一个库存商品没有锁的话会同时读到库存为 1各自扣减后库存变成 -1加了行锁后第二个请求会等第一个事务提交后才读取到库存为 0直接走到raise ValueError分支。update_fields[stock]是精细更新只回写库存字段减少不必要的数据库写入。整套流程读下来能看出作者对并发和事务是有意识的这在课程设计里属于超出平均水平的亮点。4. 把项目跑起来配置修改、数据库迁移与后台管理验证4.1 settings.py 里必改的几处数据库、语言时区、静态文件路径拿到 zip 源码直接 runserver 大概率跑不起来原因集中在settings.py没适配本地环境。我拆这份源码时的操作顺序是先打开settings.py把DATABASES从默认的sqlite3配好再看LANGUAGE_CODE和TIME_ZONE最后确认STATIC_URL和MEDIA_ROOT的目录存在。# myshop/settings.py 关键配置项 DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_I18N True USE_TZ True STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media LOGIN_URL users:login LOGIN_REDIRECT_URL shop:product_list参数说明sqlite3在课程设计和本机调试阶段是最省事的方案零配置、单文件BASE_DIR / db.sqlite3会自动在项目根目录生成数据库文件换成 MySQL 也方便ENGINE改django.db.backends.mysqlNAME、USER、PASSWORD、HOST四项补全即可但需要先安装mysqlclient并手动建库。LANGUAGE_CODE zh-hans是让 Django 自带后台显示中文界面配TIME_ZONE Asia/Shanghai才能让订单时间记录的是本地时间而不是 UTC 时间。MEDIA_ROOT对应商品图片上传落地目录urls.py里要加static()映射才能本地看到图。4.2 数据库迁移的固定套路先 makemigrations 再 migrate很多新手对 Django 迁移机制的误解是把migrate当成数据库唯一命令但manage.py makemigrations这步才是生成迁移文件的关键。源码里有现成的 app 和模型理论上可以直接migrate但如果你改动过模型字段必须先生成迁移文件。python manage.py makemigrations shop cart orders users python manage.py migrate python manage.py createsuperuser python manage.py runserver 127.0.0.1:8000建议按 app 名逐个指定生成迁移避免makemigrations把无关应用的改动也扫进去。createsuperuser是创建后台管理账号按提示输入用户名和邮箱、密码即可。runserver默认端口是 8000如果被占用可以用python manage.py runserver 127.0.0.1:8080换端口。跑完这套流程后访问http://127.0.0.1:8000/应该能看到商品列表页访问/admin/用刚才的账号登录后台。如果这一步出现TemplateDoesNotExist报错优先检查settings.py里TEMPLATES的DIRS是否指向了模板目录这是 zip 资源最常见的路径问题。4.3 用 Django Admin 给商品录入数据先让页面有内容可看商品表没数据时前端页面是一片空白所以源码里通常会顺手集成 Django Admin 的注册。合理的做法是在shop/admin.py里把商品和分类注册进去方便后台直接录入测试数据。# shop/admin.py from django.contrib import admin from .models import Category, Product admin.register(Category) class CategoryAdmin(admin.ModelAdmin): prepopulated_fields {slug: (name,)} admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display [name, category, price, stock, is_active, created_at] list_filter [is_active, category] search_fields [name, description] prepopulated_fields {slug: (name,)}prepopulated_fields会根据name自动生成 slug省去手动输入 URL 标识的麻烦。list_display决定后台列表页显示的列list_filter和search_fields是后台自带的筛选和搜索能力不加任何额外代码就能用。用后台录入几个分类和十几条商品页面就有内容可看后面验证购物车和订单流程才顺畅。如果图片上传遇上Pillow报错pip install Pillow装一下就能解决Django 的ImageField依赖这个库。5. 避坑指南Django 购物平台最常见的几类翻车现场5.1 反向解析失败NoReverseMatch报错的排查路径现象点击商品分类链接页面报NoReverseMatch at /shop/提示Reverse for product_list not found但在shop/urls.py里明明定义了同名路由。原因URL 名称冲突或app_name命名空间没有声明完整。多个应用下如果有同名nameproduct_listDjango 无法确定该用哪一个另外模板里用{% url shop:product_list %}这种带命名空间的写法时URLConf 里必须配套app_name shop。解决先检查shop/urls.py顶部有没有app_name shop这行再全局搜索有没有重复的 URL 名称做项目代码审查时我用一句 PowerShell 就能扫Select-String -Path . -Pattern nameproduct_list -Recurse把重复项全部改名。5.2 迁移后表字段对不上column ... does not exist现象运行项目后报OperationalError: table shop_product has no column named image但models.py里明明有image models.ImageField(...)。原因数据库是早期版本的表后来模型新增字段但没跑新的迁移。Django 的migrate只执行尚未应用的迁移文件如果你直接改了模型却没生成迁移数据库结构就和代码脱节了。解决按 app 逐个生成迁移再应用命令就是前面写的makemigrations shop加migrate。这里有个血泪经验课程设计拖到最后一晚才改模型改完直接runserver不迁移结果翻车。从那以后我每次改完models.py都要完整走一遍makemigrations→migrate两步哪怕只是加了一个verbose_name。5.3 图片显示不出来404 与空白图标的区分处理现象商品列表页能出来但商品图片位置一片空白或直接 404后台已经上传了图片。原因开发模式下 Django 不会自动伺服MEDIA_URL需要手动在urls.py里加static()映射。另外商品图片上传到了media/products/...但请求路径是/media/products/...两者对不上就会 404 而不是显示空白。解决在项目urls.py底部追加开发环境媒体文件路由。这块典型配置我专门记录过# myshop/urls.py 末尾加这段仅本地开发用 from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)注意这行只对DEBUGTrue生效正式部署到 Nginx 后要由 Web 服务器直接伺服媒体文件不能依赖 Django。如果图片还是空白多半是 Pillow 没装或图片本身损坏后台重新上传一张纯色图排除一下。5.4 刷新后购物车少东西session 没标记 modified现象加购商品后第一次能显示刷新页面再进购物车少了几个商品或者整个购物车清空了换个浏览器直接丢数据。原因Django 为了性能只在 request.session 被「标记为已修改」时才把变更写回存储。修改 session 字典里嵌套的内容比如cart[str(product.id)][quantity]不会自动触发保存需要手动设request.session.modified True。解决购物车相关视图里每次修改 session 内容后都显式加上这行。这是一种玄学干扰项逻辑没错但结果就是不对原因就藏在这一行。排查时可以直接在cart_add视图入口和出口分别打印request.session[cart]对比一下变更是否落库。5.5 后台中文乱码或无法登录编码与密码问题一起排除现象后台登录进去分类和商品都是乱码或者createsuperuser后登录提示密码错误。原因编码问题多半是数据库表创建时字符集不是 UTF-8SQLite 一般没这问题MySQL 就需要在建库时指定utf8mb4登录失败如果不是密码输错最常见原因是createsuperuser时没在虚拟环境里执行系统装了多个 Python 环境。解决先which python和python --version确认用的是不是项目所在虚拟环境的解释器再重新python manage.py createsuperuser。MySQL 数据库用CREATE DATABASE myshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;重建一次即可。这套操作属于环境层面的坑比代码问题更隐蔽排查顺序上应该放在前面。6. 上线前的最后一公里查询优化与订单状态走查项目能跑通只是第一步想把这份源码真正变成自己的东西还要做两件事优化数据库查询以及完整走一遍订单状态流转的验证。先看查询优化。商品列表页如果直接模板里遍历product.category.name每渲染一条商品都会额外发一条 SQL 查分类表这就是经典的 N1 查询问题。用 Django Debug Toolbar 的 SQL Panel 可以直观看到查询次数修复方式是在ListView.get_queryset里加select_relateddef get_queryset(self): queryset Product.objects.filter(is_activeTrue).select_related(category) category_slug self.kwargs.get(category_slug) if category_slug: queryset queryset.filter(category__slugcategory_slug) return querysetselect_related(category)会把外键关联的 Category 一次性 join 出来商品列表从几百条 SQL 降到一条。订单详情页反过来OrderItem要查商品信息时用prefetch_related(items__product)这是多对多和反向关联场景的解法。不要只在列表页改订单列表和购物车详情页同样要过一遍。再看订单状态验证。把后台跑起来后我习惯手动走完整条链路前台注册普通用户 → 加购商品 → 下单 → 后台把订单状态从pending改为paid→ 再改为shipped→ 最后completed。每改一个状态就刷新前台订单详情页确认状态显示的文案和颜色样式都正常。这个验证动作看起来机械却是最容易发现问题的环节——比如模板里用if order.status paid判断展示逻辑但数据库存的是已支付中文值必然怎么都对不上。这里有个第三方的迁移库值得提一句django-simple-history可以给订单状态加历史记录表每次改动都留痕这在课程设计答辩时是很好的加分项说明你考虑了数据可追溯性。安装后只需在Order模型上register一下后台就能看到状态变更的历史时间线。最后说个我的职业习惯任何 Django 项目在交付前我都会强制走一遍makemigrations --check --dry-run命令验证没有未生成的迁移文件再跑一次完整的订单链路回归。这个习惯是从一次线上事故养成的——当时改了一个模型字段忘了生成迁移测试环境因为数据量小没暴露上线后生产库直接报字段缺失整个下单功能瘫痪了半小时。从那以后我每次提交代码前都强制走一遍迁移检查和核心链路验证靠流程而不是靠记性来兜底。希望这份地图能帮你把这份 zip 源码真正跑透少踩几个我当年踩过的坑祝顺利。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑