简介这是一份基于Django框架搭建的电子商务网站完整项目源码面向已掌握Python基础、希望把MVT模式真正落地到项目中的中级Web学习者可用于课程设计、毕业设计参考。压缩包共101个文件约3.89MB以15个html页面为主体覆盖首页、智能手机、笔记本、电视等商品页与搜索、购物车、结账、注册等前台界面17个py文件承载模型、视图与路由逻辑另有sqlite3数据库文件与jpg、png等商品图片素材。项目串联起Django模板语言动态渲染、ORM建模、用户认证与权限、购物车会话保持、结账与支付对接、URLconf分发以及缓存与CSRF防护等环节还涉及部署运维思路读者可据此理清从商品展示到下单的完整链路并对照目录结构拆解各模块职责。目前已有91人浏览学习。1. Django 电商站不是“跑起来就行”从 HTML 模板到订单闭环的落地判断这套 Django 电商站源码的页面层几乎全靠 HTML 模板撑起来商品列表、详情、购物车、结算页都是模板文件改样式和改字段都绕不开它。它解决的是“从商品展示到下单”的最小闭环包含分类、商品、购物车、订单和后台管理适合已经会一点 Python、想拿一个能跑通的 Django 项目练手的人。别把它当成能直接上线的商城库存扣减、支付回调和并发控制都要自己补但作为课程设计或二次开发底稿它的目录和请求链路足够清晰。真正容易踩坑的地方不在模型写得多漂亮而在模板路径、静态文件和 session 购物车这些细节。2. 拆开 Django 电商项目的骨架模型、模板与请求链路怎么对上拿到一套 Django 电商源码先别急着改首页样式。更稳的顺序是顺着一条请求走一遍浏览器访问商品列表URL 路由找到视图视图查模型模型返回数据视图再渲染 HTML 模板。这条链路里任何一环名字对不上页面就会空白、报 404 或者模板找不到。下面按“商品列表页”这个入口反推把模型、路由、视图和模板的绑定关系拆开。2.1 从商品列表页反推数据模型商品列表页需要展示分类、商品名、价格、库存和图片所以模型层至少要落三块分类、商品、订单。常见做法是把分类和商品做成一对多把订单和订单项拆成两张表订单项保存下单时的商品价格快照避免商品调价后历史订单金额跟着变。Django 的DecimalField比FloatField更适合价格ImageField需要 Pillow 支持slug用来做可读 URL。下面这段模型是这类项目里最常出现的结构字段名可以按源码调整但关系设计值得先看懂。from django.db import models from django.urls import reverse class Category(models.Model): name models.CharField(max_length100) slug models.SlugField(max_length100, uniqueTrue) class Meta: ordering [name] def __str__(self): return self.name class Product(models.Model): category models.ForeignKey( Category, on_deletemodels.CASCADE, related_nameproducts ) name models.CharField(max_length200) slug models.SlugField(max_length200, uniqueTrue) price models.DecimalField(max_digits10, decimal_places2) stock models.PositiveIntegerField(default0) image models.ImageField(upload_toproducts/, blankTrue) is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] def __str__(self): return self.name def get_absolute_url(self): return reverse(store:product_detail, args[self.slug])这段代码里最值得关注的是三个参数on_deletemodels.CASCADE表示分类删除后商品跟着删实际项目里如果不想误删商品可以改成PROTECT或SET_NULLrelated_nameproducts让视图里可以用category.products.all()反查商品upload_toproducts/决定图片落在MEDIA_ROOT/products/下。价格用DecimalField(max_digits10, decimal_places2)表示最多 10 位、保留 2 位小数别用 float 存金额否则后面算总价会出现0.30000000000000004这种玄学尾数。订单模型通常拆成Order和OrderItem。Order存收货信息、创建时间、支付状态OrderItem存订单、商品、下单单价和数量。下单单价一定要单独存一份不要只靠外键回查商品当前价格。这个细节在课程设计里经常被忽略等到改价测试时才发现历史订单金额全变了。class Order(models.Model): first_name models.CharField(max_length50) last_name models.CharField(max_length50) email models.EmailField() address models.CharField(max_length250) city models.CharField(max_length100) created_at models.DateTimeField(auto_now_addTrue) paid models.BooleanField(defaultFalse) status models.CharField( max_length20, choices[(created, 已创建), (paid, 已支付), (shipped, 已发货)], defaultcreated ) class Meta: ordering [-created_at] class OrderItem(models.Model): order models.ForeignKey(Order, related_nameitems, on_deletemodels.CASCADE) product models.ForeignKey(Product, related_nameorder_items, on_deletemodels.PROTECT) price models.DecimalField(max_digits10, decimal_places2) quantity models.PositiveIntegerField(default1)OrderItem.product用PROTECT是为了防止商品被删后订单项变成空指针price是下单时的快照quantity用正整数避免负数订单。模型定完先跑迁移不要一边改模型一边手动改数据库后面迁移冲突会很麻烦。2.2 URL、视图与 HTML 模板的绑定关系Django 的请求链路靠 URLconf 串起来。商品列表、分类筛选、商品详情一般会放在一个应用级urls.py里再用include()挂到项目根路由。视图函数只做三件事取数据、组装上下文、渲染模板。模板里再用{% for %}和{{ }}把数据铺到 HTML 结构里。下面是一组常见写法。# store/urls.py from django.urls import path from . import views app_name store urlpatterns [ path(, views.product_list, nameproduct_list), path(category/slug:category_slug/, views.product_list, nameproduct_list_by_category), path(slug:slug/, views.product_detail, nameproduct_detail), ]# store/views.py from django.shortcuts import render, get_object_or_404 from .models import Category, Product def product_list(request, category_slugNone): category None categories Category.objects.all() products Product.objects.filter(is_activeTrue) if category_slug: category get_object_or_404(Category, slugcategory_slug) products products.filter(categorycategory) return render(request, store/product_list.html, { category: category, categories: categories, products: products, }) def product_detail(request, slug): product get_object_or_404(Product, slugslug, is_activeTrue) return render(request, store/product_detail.html, {product: product})app_name store是命名空间模板里用{% url store:product_detail product.slug %}就不会和别的应用重名。get_object_or_404比Product.objects.get()更稳查不到直接返回 404不会抛 500。is_activeTrue是软删除思路后台下架商品不用物理删除。模板层最常见的翻车是路径写错。render(request, store/product_list.html)会去TEMPLATES[DIRS]和每个应用的templates/目录下找。如果模板放在项目根目录的templates/store/就要在settings.py里把BASE_DIR / templates加进DIRS如果放在store/templates/store/Django 会自动发现但目录名多一层少一层都会报TemplateDoesNotExist。{% extends base.html %} {% load static %} {% block title %}商品列表{% endblock %} {% block content %} div classproduct-grid {% for product in products %} a href{{ product.get_absolute_url }} {% if product.image %} img src{{ product.image.url }} alt{{ product.name }} {% else %} img src{% static img/placeholder.png %} alt占位图 {% endif %} h3{{ product.name }}/h3 p¥{{ product.price }}/p /a {% empty %} p暂无商品/p {% endfor %} /div {% endblock %}模板里{% extends base.html %}必须放在第一行前面不能有 HTML 注释或空输出{% load static %}用来加载静态文件标签product.image.url只有在图片存在时才安全所以外面套{% if product.image %}。{% empty %}是for循环的空分支商品查不到时给用户一个提示比空白页好排查。2.3 购物车为什么优先走 session 而不是直接落库电商站购物车有两种常见做法登录用户落库游客用 session。这套源码更常见的是 session 购物车原因是游客还没登录直接建Cart表会带来大量匿名脏数据而且合并购物车的逻辑更复杂。session 购物车把商品 ID、数量、单价存进request.session键名用一个常量控制读写都围绕这个字典。缺点是换浏览器或清 session 后购物车丢失所以结算前要引导登录或至少保留 session。# store/cart.py from decimal import Decimal from django.conf import settings from .models import Product class Cart: def __init__(self, request): self.session request.session cart self.session.get(settings.CART_SESSION_ID) if not cart: cart self.session[settings.CART_SESSION_ID] {} self.cart cart def add(self, product, quantity1, override_quantityFalse): product_id str(product.id) if product_id not in self.cart: self.cart[product_id] { quantity: 0, price: str(product.price), } if override_quantity: self.cart[product_id][quantity] quantity else: self.cart[product_id][quantity] quantity self.save() def save(self): self.session.modified True def remove(self, product): product_id str(product.id) if product_id in self.cart: del self.cart[product_id] self.save() def __iter__(self): product_ids self.cart.keys() products Product.objects.filter(id__inproduct_ids) cart self.cart.copy() for product in products: cart[str(product.id)][product] product for item in cart.values(): item[price] Decimal(item[price]) item[total_price] item[price] * item[quantity] yield item def get_total_price(self): return sum(Decimal(item[price]) * item[quantity] for item in self.cart.values())settings.CART_SESSION_ID需要在settings.py里定义比如CART_SESSION_ID cart。save()里必须写self.session.modified True否则 Django 可能认为 session 没变刷新后购物车加不进去。__iter__里把价格转回Decimal因为 session 序列化后价格是字符串。get_total_price()用Decimal累加避免金额尾数问题。session 购物车适合课程设计和中小项目真要做多端同步再考虑登录后落库并合并。3. 本地复现的完整路径环境、数据库与静态资源一步不跳源码能不能用第一道门槛不是代码写得好不好而是本地能不能稳定跑起来。Django 项目的复现路径比较固定建虚拟环境、装依赖、改 settings、迁移数据库、建超级用户、启动开发服务器。每一步都有容易漏的参数尤其是静态目录和媒体目录漏了页面能打开但样式和图片全丢。3.1 依赖安装与 settings.py 的最小改动先建虚拟环境避免把依赖装进系统 Python。下面命令在项目根目录执行Windows 和 macOS/Linux 的激活命令不一样按自己的终端选。# 创建虚拟环境 python -m venv .venv # macOS / Linux 激活 source .venv/bin/activate # Windows PowerShell 激活 # .venv\Scripts\Activate.ps1 # 安装依赖源码里有 requirements.txt 就按文件装 pip install -r requirements.txt # 如果没有 requirements.txt至少装 Django 和 Pillow pip install django pillowpython -m venv .venv会在项目下生成隔离环境pip install -r requirements.txt按源码锁定的依赖装不要自己随便升级 Django 大版本Pillow是ImageField的依赖不装会在迁移或上传图片时报错。装完用python -m django --version确认版本再打开settings.py做最小改动。# settings.py 关键片段 from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, store, # 电商应用 ] TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], # 项目级模板目录 APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.debug, django.template.context_processors.request, django.contrib.auth.context_processors.auth, django.contrib.messages.context_processors.messages, ], }, }, ] STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media CART_SESSION_ID cart LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ TrueDIRS决定项目级模板去哪找APP_DIRSTrue让 Django 自动扫描应用下的templates/STATICFILES_DIRS是开发阶段的静态文件目录MEDIA_ROOT是上传图片的落盘位置CART_SESSION_ID是购物车 session 键名。下面这张表把几个容易混的参数放在一起对照。参数作用常见错误TEMPLATES[DIRS]指定项目级模板目录只写templates不写BASE_DIR /路径解析不到STATIC_URL静态文件访问前缀模板里写死/static/改前缀后全 404STATICFILES_DIRS开发阶段额外静态目录和STATIC_ROOT混用collectstatic时报错MEDIA_ROOT上传文件保存目录只配MEDIA_URL不配MEDIA_ROOT图片存不进去CART_SESSION_ID购物车 session 键代码里写死字符串改配置后读不到旧购物车注意STATIC_ROOT和STATICFILES_DIRS不是一回事。开发阶段用STATICFILES_DIRS找静态文件部署时collectstatic收集到STATIC_ROOT两个目录不要指向同一个位置。3.2 迁移、超级用户与开发服务器启动数据库迁移是 Django 项目跑起来的关键步骤。源码里如果自带db.sqlite3可以先备份再迁移如果没有直接按模型生成。迁移前确认INSTALLED_APPS里已经加入电商应用否则makemigrations找不到模型。# 生成迁移文件应用名按源码实际名字改 python manage.py makemigrations store # 执行迁移建表 python manage.py migrate # 创建后台超级用户按提示输入用户名和密码 python manage.py createsuperuser # 启动开发服务器 python manage.py runservermakemigrations store只生成电商应用的迁移文件migrate会把所有未执行的迁移跑到数据库createsuperuser用来登录/admin/runserver默认监听127.0.0.1:8000。如果 8000 端口被占用可以换成python manage.py runserver 8001。启动后先访问首页和/admin/首页能打开说明路由和模板基本通了后台能登录说明数据库和认证表正常。有时候迁移会提示“No changes detected”常见原因是应用没加进INSTALLED_APPS或者模型文件没保存。还有一种情况是源码自带迁移文件但数据库里已经有旧表直接migrate会报表已存在。这个时候不要上来就删数据库先用python manage.py showmigrations看哪些迁移未应用再决定是回滚还是做迁移合并。3.3 媒体文件与静态文件目录的配置开发阶段图片不显示八成是媒体路由没挂。MEDIA_ROOT只决定文件存哪浏览器要访问/media/还需要在根urls.py里加静态服务。下面这段只在DEBUGTrue时生效生产环境不要这么干。# 项目根 urls.py from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns [ path(admin/, admin.site.urls), path(, include(store.urls, namespacestore)), ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)include(store.urls, namespacestore)把应用路由挂到根路径static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)让开发服务器把/media/映射到上传目录。少了这段后台能上传图片但前台img src{{ product.image.url }}会 404。静态文件 CSS 不生效也是同理检查STATICFILES_DIRS和模板里有没有{% load static %}。部署前还要跑一次python manage.py collectstatic把静态文件收进STATIC_ROOT。开发阶段runserver会自动服务静态文件但DEBUGFalse后它不再管这些请求需要 Nginx 或 WhiteNoise 接管。这个切换点经常让新手以为“本地好好的一上线样式全没了”。4. 商品到订单的闭环购物车、结算与后台管理商品列表能打开只是前半程电商站真正要验证的是“加购物车、改数量、填地址、生成订单、后台能看到”这条闭环。这条链路里金额计算、库存扣减、重复提交和后台配置是最容易出问题的地方。下面按购物车、订单、后台三块拆。4.1 购物车加减与金额计算购物车页要展示每项商品的小计和总价还要支持加数量、减数量、删除。视图层通常用product_id和quantity两个 POST 参数配合Cart类的add、remove方法。数量为 0 时直接删除不要留一条数量为 0 的记录。# store/views.py from django.shortcuts import get_object_or_404, redirect from django.views.decorators.http import require_POST from .models import Product from .cart import Cart require_POST def cart_add(request, product_id): cart Cart(request) product get_object_or_404(Product, idproduct_id) quantity int(request.POST.get(quantity, 1)) override request.POST.get(override) True cart.add(productproduct, quantityquantity, override_quantityoverride) return redirect(store:cart_detail) require_POST def cart_remove(request, product_id): cart Cart(request) product get_object_or_404(Product, idproduct_id) cart.remove(product) return redirect(store:cart_detail)require_POST限制只能 POST 访问避免通过 GET 链接误加购物车quantity从表单取默认 1override用来区分“加一件”和“直接改成某个数量”。redirect回到购物车页刷新不会重复提交。金额计算放在Cart.__iter__和get_total_price()里模板只负责展示不要把乘法散落在 HTML 里。购物车模板里每个数量输入框都要带namequantity加减按钮用两个独立表单避免一个表单里多个提交按钮逻辑混乱。金额展示用¥{{ item.total_price }}不要在前端用 JavaScript 再算一遍前后端算法不一致时对账很痛苦。4.2 订单创建与重复提交防护结算页提交后要创建Order和OrderItem并把购物车清空。这里有两个关键点一是用事务包住订单和订单项避免订单建了但订单项没写进去二是防重复提交用户刷新结算页不能生成两笔订单。常见做法是 POST 成功后重定向到订单详情页也就是 PRG 模式。# store/views.py from django.db import transaction from .models import OrderItem from .forms import OrderCreateForm def order_create(request): cart Cart(request) if request.method POST: form OrderCreateForm(request.POST) if form.is_valid(): with transaction.atomic(): order form.save() for item in cart: OrderItem.objects.create( orderorder, productitem[product], priceitem[price], quantityitem[quantity], ) cart.clear() return redirect(store:order_detail, order.id) else: form OrderCreateForm() return render(request, store/order_create.html, {cart: cart, form: form})transaction.atomic()保证订单和订单项要么都成功要么都回滚item[price]用的是购物车里的快照价cart.clear()要在订单项写完后执行redirect到订单详情页刷新不会重复 POST。防重复提交还可以加唯一订单号或幂等键但课程设计里 PRG 已经能挡住大部分误操作。库存扣减要谨慎。简单项目可以在订单创建后product.stock - quantity但并发下会超卖。更稳的做法是用select_for_update()锁行或者下单时只校验库存、支付成功后再扣。这个边界要在代码注释里写清楚别让后面维护的人以为已经处理了并发。4.3 Django Admin 的商品与订单运营配置后台管理是这套源码的加分项。商品上架、库存修改、订单查看都能在 Admin 里完成。默认注册模型只有增删改查稍微配置一下list_display、list_filter、search_fields就能好用很多。# store/admin.py from django.contrib import admin from .models import Category, Product, Order, OrderItem admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display [name, slug] prepopulated_fields {slug: (name,)} admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display [name, category, price, stock, is_active] list_filter [is_active, category] search_fields [name] prepopulated_fields {slug: (name,)} class OrderItemInline(admin.TabularInline): model OrderItem raw_id_fields [product] admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display [id, first_name, email, status, paid, created_at] list_filter [paid, status, created_at] inlines [OrderItemInline]prepopulated_fields让 slug 跟着名称自动填list_filter方便按上架状态和分类筛商品search_fields支持后台搜索OrderItemInline把订单项嵌在订单详情页里不用来回跳。raw_id_fields在商品多的时候比下拉框快很多。后台配置完至少走一遍“新增分类 → 新增商品 → 前台加购 → 下单 → 后台看订单”的流程确保闭环通了。5. 避坑排查Django 电商站最常见的五类翻车现场这类项目本地跑通不难难在页面看着正常、功能却悄悄坏掉。下面五类问题是血泪经验里出现频率最高的每一条都按“现象 → 原因 → 解决”拆开。遇到问题时先按顺序排查不要一上来就删数据库或重装依赖。5.1 静态文件 404 与图片不显示现象首页能打开但 CSS 没生效商品图片全是裂图浏览器控制台一堆 404。原因通常有两个模板里静态路径写错或者开发服务器没挂媒体路由。STATIC_URL配了但模板里手写/static/css/main.css目录结构一变就断图片上传成功但urls.py没加static(settings.MEDIA_URL, ...)/media/products/xxx.jpg直接 404。解决模板里统一用{% load static %}加{% static css/main.css %}不要手写路径图片用{{ product.image.url }}并套{% if product.image %}开发环境在根路由加媒体路由部署环境跑collectstatic并让 Nginx 或 WhiteNoise 接管。还有一个容易漏的点MEDIA_ROOT目录没有写权限上传时直接报错检查目录权限和磁盘空间。5.2 CSRF 校验失败与表单提交现象提交购物车、结算或后台表单时返回 403页面提示 CSRF verification failed。原因模板表单里漏了{% csrf_token %}或者 AJAX 请求没带 CSRF token也有可能是CSRF_TRUSTED_ORIGINS没配跨域或反向代理后 origin 对不上。解决所有 POST 表单内部加{% csrf_token %}AJAX 从 cookie 里取csrftoken放到X-CSRFToken请求头反向代理场景检查CSRF_TRUSTED_ORIGINS和ALLOWED_HOSTS。不要图省事用csrf_exempt把整个视图豁免掉购物车和订单接口豁免 CSRF 等于把提交入口敞开。5.3 session 购物车丢失与时区/金额问题现象加购后刷新购物车空了或者金额出现99.99000000000001订单时间比本地时间差 8 小时。原因Cart.save()没设session.modified TrueDjango 没保存 session金额用 float 计算TIME_ZONE没改或者模板里没做时区转换。解决save()里补self.session.modified True价格统一用Decimalsession 里存字符串取出后转DecimalTIME_ZONE设成Asia/ShanghaiUSE_TZTrue时模板里用{{ order.created_at|date:Y-m-d H:i }}展示。session 购物车还依赖django.contrib.sessions和session表迁移没跑全也会丢。5.4 迁移冲突与模板继承失效现象改完模型跑makemigrations报冲突或者页面报TemplateDoesNotExist、TemplateSyntaxError: extends。原因多人改模型导致迁移分叉模板目录没加进TEMPLATES[DIRS]{% extends %}没放在模板第一行前面有空白或注释输出。解决迁移冲突先showmigrations看状态能合并就makemigrations --merge不要直接删迁移文件模板路径用BASE_DIR / templates明确指过去{% extends %}放第一行前面不要有任何输出base.html里留好{% block content %}子模板只覆盖块不要重复写html结构。模板继承坏了页面往往不是报错而是样式叠两层排查时先看渲染后的 HTML 源码。5.5 订单重复提交与库存超卖现象用户刷新结算页后台多出两笔一样的订单或者两个用户同时买最后一件库存变成负数。原因POST 后直接render没有重定向库存扣减没有事务和锁订单创建和库存更新不在一个原子操作里。解决订单创建成功后redirect到详情页用 PRG 模式挡住刷新重复提交订单和订单项用transaction.atomic()包住库存扣减用select_for_update()锁行或者下单只校验、支付回调里再扣。课程设计里至少要在订单表加唯一约束或幂等键别等演示时刷新出两笔订单才补。6. 把项目改成可验证的交付件模板继承、测试与部署前检查这套源码最值得花时间的地方不是再加一个页面而是把它改成“能验证”的状态。我一般先统一模板继承把所有页面公共的头部、导航、底部放进base.html子模板只留{% block content %}这样改一次导航全站生效不会出现首页和详情页两套 HTML 结构。然后补三条最小测试盯住购物车金额、订单创建和库存扣减。测试不用写多能挡住“改完模板把金额算错”这类低级问题就够。# store/tests.py from decimal import Decimal from django.test import TestCase from .models import Product, Category class CartTotalTest(TestCase): def setUp(self): category Category.objects.create(name测试分类, slugtest) self.product Product.objects.create( categorycategory, name测试商品, slugtest-product, priceDecimal(19.90), stock10, ) def test_cart_total_uses_decimal(self): from .cart import Cart request self.client.request().wsgi_request request.session {} cart Cart(request) cart.add(self.product, quantity3) self.assertEqual(cart.get_total_price(), Decimal(59.70))这段测试的关键是Decimal(19.90)和Decimal(59.70)用字符串构造避免浮点误差request.session {}是测试里模拟 session 的简化写法真实项目可以用self.client走完整请求。测试跑通后再改模板心里有底。部署前再走一遍检查表DEBUGFalse、ALLOWED_HOSTS填对、SECRET_KEY不留在源码里、静态文件已collectstatic、媒体目录有备份、后台超级用户密码改掉。这套源码包适合拿来改不适合直接卖货我现在每次拿到这类 Django 电商项目都先跑迁移和三条测试再动 HTML后悔药少吃很多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?