简介:这是一份基于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_length=100) slug = models.SlugField(max_length=100, unique=True) class Meta: ordering = ['name'] def __str__(self): return self.name class Product(models.Model): category = models.ForeignKey( Category, on_delete=models.CASCADE, related_name='products' ) name = models.CharField(max_length=200) slug = models.SlugField(max_length=200, unique=True) price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField(default=0) image = models.ImageField(upload_to='products/', blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) 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_delete=models.CASCADE表示分类删除后商品跟着删,实际项目里如果不想误删商品,可以改成PROTECT或SET_NULL;related_name='products'让视图里可以用category.products.all()反查商品;upload_to='products/'决定图片落在MEDIA_ROOT/products/下。价格用DecimalField(max_digits=10, decimal_places=2),表示最多 10 位、保留 2 位小数,别用 float 存金额,否则后面算总价会出现0.30000000000000004这种玄学尾数。
订单模型通常拆成Order和OrderItem。Order存收货信息、创建时间、支付状态;OrderItem存订单、商品、下单单价和数量。下单单价一定要单独存一份,不要只靠外键回查商品当前价格。这个细节在课程设计里经常被忽略,等到改价测试时才发现历史订单金额全变了。
class Order(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) email = models.EmailField() address = models.CharField(max_length=250) city = models.CharField(max_length=100) created_at = models.DateTimeField(auto_now_add=True) paid = models.BooleanField(default=False) status = models.CharField( max_length=20, choices=[('created', '已创建'), ('paid', '已支付'), ('shipped', '已发货')], default='created' ) class Meta: ordering = ['-created_at'] class OrderItem(models.Model): order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) product = models.ForeignKey(Product, related_name='order_items', on_delete=models.PROTECT) price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField(default=1)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, name='product_list'), path('category/<slug:category_slug>/', views.product_list, name='product_list_by_category'), path('<slug:slug>/', views.product_detail, name='product_detail'), ]# store/views.py from django.shortcuts import render, get_object_or_404 from .models import Category, Product def product_list(request, category_slug=None): category = None categories = Category.objects.all() products = Product.objects.filter(is_active=True) if category_slug: category = get_object_or_404(Category, slug=category_slug) products = products.filter(category=category) return render(request, 'store/product_list.html', { 'category': category, 'categories': categories, 'products': products, }) def product_detail(request, slug): product = get_object_or_404(Product, slug=slug, is_active=True) 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_active=True是软删除思路,后台下架商品不用物理删除。
模板层最常见的翻车是路径写错。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 class="product-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, quantity=1, override_quantity=False): 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__in=product_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_DIRS=True让 Django 自动扫描应用下的templates/;STATICFILES_DIRS是开发阶段的静态文件目录;MEDIA_ROOT是上传图片的落盘位置;CART_SESSION_ID是购物车 session 键名。下面这张表把几个容易混的参数放在一起对照。
| 参数 | 作用 | 常见错误 |
|---|---|---|
TEMPLATES['DIRS'] | 指定项目级模板目录 | 只写templates不写BASE_DIR /,路径解析不到 |
STATIC_URL | 静态文件访问前缀 | 模板里写死/static/,改前缀后全 404 |
STATICFILES_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里加静态服务。下面这段只在DEBUG=True时生效,生产环境不要这么干。
# 项目根 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', namespace='store')), ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)include('store.urls', namespace='store')把应用路由挂到根路径;static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)让开发服务器把/media/映射到上传目录。少了这段,后台能上传图片,但前台<img src="{{ product.image.url }}">会 404。静态文件 CSS 不生效也是同理,检查STATICFILES_DIRS和模板里有没有{% load static %}。
部署前还要跑一次python manage.py collectstatic,把静态文件收进STATIC_ROOT。开发阶段runserver会自动服务静态文件,但DEBUG=False后它不再管这些请求,需要 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, id=product_id) quantity = int(request.POST.get('quantity', 1)) override = request.POST.get('override') == 'True' cart.add(product=product, quantity=quantity, override_quantity=override) return redirect('store:cart_detail') @require_POST def cart_remove(request, product_id): cart = Cart(request) product = get_object_or_404(Product, id=product_id) cart.remove(product) return redirect('store:cart_detail')@require_POST限制只能 POST 访问,避免通过 GET 链接误加购物车;quantity从表单取,默认 1;override用来区分“加一件”和“直接改成某个数量”。redirect回到购物车页,刷新不会重复提交。金额计算放在Cart.__iter__和get_total_price()里,模板只负责展示,不要把乘法散落在 HTML 里。
购物车模板里每个数量输入框都要带name="quantity",加减按钮用两个独立表单,避免一个表单里多个提交按钮逻辑混乱。金额展示用¥{{ 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( order=order, product=item['product'], price=item['price'], quantity=item['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 = True,Django 没保存 session;金额用 float 计算;TIME_ZONE没改,或者模板里没做时区转换。
解决:save()里补self.session.modified = True;价格统一用Decimal,session 里存字符串,取出后转Decimal;TIME_ZONE设成Asia/Shanghai,USE_TZ=True时模板里用{{ 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='测试分类', slug='test') self.product = Product.objects.create( category=category, name='测试商品', slug='test-product', price=Decimal('19.90'), stock=10, ) 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, quantity=3) self.assertEqual(cart.get_total_price(), Decimal('59.70'))这段测试的关键是Decimal('19.90')和Decimal('59.70'),用字符串构造避免浮点误差;request.session = {}是测试里模拟 session 的简化写法,真实项目可以用self.client走完整请求。测试跑通后再改模板,心里有底。部署前再走一遍检查表:DEBUG=False、ALLOWED_HOSTS填对、SECRET_KEY不留在源码里、静态文件已collectstatic、媒体目录有备份、后台超级用户密码改掉。这套源码包适合拿来改,不适合直接卖货;我现在每次拿到这类 Django 电商项目,都先跑迁移和三条测试再动 HTML,后悔药少吃很多。希望帮到你。
本文还有配套的精品资源,点击获取