news 2026/10/13 3:50:22

Django电商实战:从订单模型到并发扣库存的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django电商实战:从订单模型到并发扣库存的完整指南

简介:EcommerceWbsite 是基于 Python Django 框架的电子商务网站完整项目,面向 Django 入门与中级 Web 开发者,尤其适合需要课程设计或实战项目参考的人群。项目前端以 HTML 与 Django 模板语言(DTL)结合实现,覆盖商品分类浏览、搜索、详情展示、购物车、结算与订单跟踪等电商核心业务。资源共 101 个文件,以 Python 源码(.py)、HTML 页面、JPG/WebP/PNG 图片素材为主,同时附带 SQLite 数据库文件与少量 pyc 缓存文件,压缩包仅 3.89MB,结构轻量、便于下载后快速运行与学习。目前已有 91 人学习下载。通过该项目可以系统掌握 Django 的 MVT 架构、ORM 数据库建模、URL 路由与视图函数、用户认证与权限管理,以及购物车持久化、支付网关集成、缓存与安全防护等关键开发技能;资源中的 HTML 页面覆盖首页、结算、注册、订单追踪等主要场景,对于理解前后端交互和电商站点部署也具有直接参考价值。

1. 用Django制作电子商务网站,到底是在做什么

很多开发者拿到“Django制作的电子商务网站”这类需求时,第一反应是去找一个开源项目改改。但真正投入过的人都明白,一个能支撑商品、购物车、订单、支付回调全流程的Django电商站点,核心难点从来不在“页面长什么样”,而在数据模型怎么设计、订单状态怎么流转、并发下库存怎么不超卖。下面基于实际开发经验,把从零搭建Django电商项目的路径拆开讲——技术选型、数据模型、购物车与订单实现、支付回调幂等、踩坑记录、上线前安全与性能清单,全部落到可执行的代码和参数上。适合两类人:一是想用Django完整做一次电商练手项目的初学者,二是在公司内部做商城类系统、需要快速评估方案可行性的后端工程师。

2. 搭建项目骨架与数据模型:先定好“订单如何流转”,再写View

电商网站和普通内容站最大的区别是:数据间的关系是有钱在流动的。商品和购物车之间、购物车和订单之间、订单和支付之间的每一步,都必须可追溯。所以在创建第一个View之前,先把Django项目结构想好。

2.1 项目的App划分:一个功能一个App,别把Model都塞进一个文件

我习惯把电商站点拆成五个App:products、cart、orders、payment、accounts。这样做的好处是每个App的models.py、views.py、urls.py边界清晰,后续做权限控制、缓存策略和测试时不会互相干扰。创建命令如下:

django-admin startproject ecommerce_shop python manage.py startapp products python manage.py startapp cart python manage.py startapp orders python manage.py startapp payment python manage.py startapp accounts

创建完之后,把五个App写进settings.py的INSTALLED_APPS里。这里有一个很多人踩过的坑:Django 4之后,INSTALLED_APPS里每增加一个App,都要同步确认App之间没有循环引用。商品模型Product被订单项OrderItem引用,OrderItem又被Order引用,这三者之间的外键关系如果不画清楚,后面做序列化和统计查询时会出现一堆性能问题。

2.2 商品模型:SKU、价格、库存三件套,保存历史价格是常识

下面是我常用的Product模型设计:

from django.db import models from django.urls import reverse class Category(models.Model): name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=120, unique=True) def get_absolute_url(self): return reverse('products:category_list', kwargs={'slug': self.slug}) def __str__(self): return self.name class Meta: verbose_name_plural = 'categories' class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products') name = models.CharField(max_length=200, db_index=True) slug = models.SlugField(max_length=200, unique=True) sku = models.CharField(max_length=50, unique=True, verbose_name='SKU编码') price = models.DecimalField(max_digits=10, decimal_places=2) old_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) stock = models.PositiveIntegerField(default=0) image = models.ImageField(upload_to='products/%Y/%m/', blank=True) description = models.TextField(blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.name

几个设计要点说明一下。价格用DecimalField不用FloatField,因为浮点精度在金额计算上会出问题;old_price字段用来支持“划线价”这种电商常见展示,比临时去算历史最低价省事得多。stock字段是库存数量,后续章节会专门说并发扣减的问题。sku字段设置unique=True,这是电商系统里绕不开的约束——同一个商品可以有多个颜色或尺寸,但SKU必须唯一,否则订单环节对不上货。

分类外键用on_delete=models.PROTECT而不是CASCADE,是为了防止分类被误删时连带商品被清空。电商数据属于敏感数据,宁可报错也不该静默删除。

2.3 订单模型:订单项要保存“商品快照”,不是单纯存外键

大多数初学者会这样设计订单项:OrderItem里放一个ForeignKey指向Product,下单的时候直接关联商品表。这在演示项目里没问题,但一旦商品改了价格或者被下架,历史订单的金额就对不上了。正确做法是冗余一份商品信息快照到订单项里。

from django.db import models from django.conf import settings class Order(models.Model): class Status(models.TextChoices): CREATED = 'CREATED', '已创建' PAID = 'PAID', '已支付' SHIPPED = 'SHIPPED', '已发货' COMPLETED = 'COMPLETED', '已完成' CANCELLED = 'CANCELLED', '已取消' user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='orders') order_number = models.CharField(max_length=32, unique=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.CREATED) total_amount = models.DecimalField(max_digits=10, decimal_places=2) shipping_address = models.TextField() created_at = models.DateTimeField(auto_now_add=True) paid_at = models.DateTimeField(blank=True, null=True) class Meta: ordering = ['-created_at'] class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, related_name='order_items') product_name = models.CharField(max_length=200) product_sku = models.CharField(max_length=50) unit_price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField(default=1) @property def subtotal(self): return self.unit_price * self.quantity

这段代码里最关键的是OrderItem中的product_name、product_sku、unit_price三个字段。它们是在下单那一刻从Product表复制过来的,之后无论商品表怎么改,这条订单的历史数据都不会被污染。外键product字段设置为SET_NULL,意味着商品被删除时订单记录仍然保留,只是关联为空。order_number必须全局唯一,我一般用时间戳加随机数生成,格式类似20250113103012001。

订单状态用Django的TextChoices枚举来做,比搞一张独立的订单状态表在中小型项目里更实用。状态机的流转建议只允许“已创建→已支付→已发货→已完成”这条正向路径,取消操作单独走CANCELLED,不要允许任意跳转,否则售后对账时容易乱。

写完这些模型,执行迁移:

python manage.py makemigrations products orders cart python manage.py migrate

迁移过程中如果报“字段类型冲突”或“依赖未找到”的错误,多数情况是App之间外键引用顺序写反了,调整一下models.py里外键声明的顺序再重跑即可。

3. 商品浏览与购物车实现:先把用户看得见摸得着的流程打通

数据模型立住之后,开始写业务逻辑。商品列表、商品详情、购物车增删改查,这几条链路是电商的“生命线”,也是后面订单流程的入口。这个阶段的目标不是炫技,而是把用户操作路径用最直接可靠的方式跑通。

3.1 商品列表与详情:用Django ORM做一次查询优化

刚开始做商品列表时,直接用ListView拿到全部商品对象,然后模板里循环出分类、商品名、价格。数据量一上来就发现问题:每展示一个商品都要查一次分类表,N+1查询把接口拖到几百毫秒。解决办法很简单,在QuerySet上加上select_related,把外键关联的对象一次性查出来。

from django.views.generic import ListView from .models import Product class ProductListView(ListView): model = Product template_name = 'products/product_list.html' context_object_name = 'products' paginate_by = 12 def get_queryset(self): qs = Product.objects.select_related('category').filter(is_active=True) qs = qs.only('name', 'slug', 'price', 'old_price', 'image', 'category__name') return qs

only在这里的作用是限定只取列表页需要的字段,避免把description这种大字段也读出来占用内存。paginate_by=12让Django帮我们处理翻页逻辑。前后端不分离的Django项目里,这种写法是性能和开发效率最均衡的做法。

商品详情页同样要注意查询效率,而且要比列表页多做一个动作:把库存字段放入查询。不要遍历查询每个SKU的库存,直接用一条annotate把最新的可用库存聚合出来,减少DB命中次数。

3.2 购物车设计:会话优先,登录后合并,还是直接存数据库

购物车实现路线有两条。一条是纯session方案:购物车数据放在request.session里,用户未登录也能加购,但换设备数据就丢了;另一条是纯数据库方案:购物车表关联用户,登录后跨设备同步,但匿名用户购物车就需要一个临时标识来关联。我一般会在中小型电商项目里用“session为主,登录后合并进数据库”的混合方案。

# cart/services.py from django.conf import settings def add_to_cart(request, product_id, quantity=1): cart = request.session.setdefault('cart', {}) key = str(product_id) if key in cart: cart[key]['quantity'] += quantity else: cart[key] = {'quantity': quantity} request.session.modified = True def merge_cart_to_db(request, user_cart): session_cart = request.session.get('cart', {}) for product_id, item in session_cart.items(): user_cart.add_item(product_id, item['quantity']) request.session['cart'] = {} request.session.modified = True

这段代码里有个容易被忽略的细节:request.session.modified = True。Django默认只有修改或键新增时才判定session需要保存,如果你只修改了嵌套dict内部的值,框架并不会自动感知,必须手动标记。这个现象很隐蔽,表现是用户加了商品,刷新后购物车还是空的,属于典型的不报错但逻辑不对的问题。

混合方案在用户体验上的取舍是:匿名用户购物车数据保留在session里,有效期由SESSION_COOKIE_AGE控制,我一般设成7天。用户登录时,通过一个信号把session购物车合并进数据库购物车,然后清空session。库存校验在加入购物车时就做一次,走到订单创建环节再校验一次,双保险。

3.3 购物车视图与模板渲染:计算总价时的Decimal陷阱

购物车页面通常要展示每个商品的小计和整车总价。如果用Python原生的float去做加法,会出现0.1+0.2这类精度问题,体现在金额上就是总价偶尔差一分钱。正确做法是全链路用Decimal。

from decimal import Decimal def cart_total(cart_items): total = Decimal('0.00') for item in cart_items: total += item['line_total'] return total.quantize(Decimal('0.01'))

模板里渲染时用Django内置的floatformat过滤器,或者直接在模型属性里返回格式化字符串。千万不要在模板里做加减法,模板表达式不参与Decimal上下文管理,容易把精度问题放大。

4. 从购物车到订单:事务、库存扣减与支付回调

购物车阶段用户反悔成本低,数据也无所谓。但用户一旦点击“提交订单”,后面每一步都必须可追溯、可恢复。这是电商项目从“能演示”到“能上线”的关键分水岭。

4.1 创建订单的事务边界:库存扣减与订单生成必须原子化

创建订单时,需要同时做三件事:从购物车生成订单项、扣减库存、清空购物车。任何一步失败,都不能让另外两步落库。实现方式用Django的transaction.atomic包起来。

from django.db import transaction from django.shortcuts import get_object_or_404 from decimal import Decimal @transaction.atomic def create_order_from_cart(request, shipping_address): cart_items = get_cart_items(request.user) if not cart_items: raise ValueError('购物车为空') total = Decimal('0.00') order = Order.objects.create( user=request.user, order_number=generate_order_number(), status=Order.Status.CREATED, total_amount=Decimal('0.00'), shipping_address=shipping_address, ) order_items = [] for item in cart_items: product = get_object_or_404(Product, id=item.product_id) if product.stock < item.quantity: raise ValueError(f'商品{product.name}库存不足') product.stock -= item.quantity product.save(update_fields=['stock']) order_items.append( OrderItem( order=order, product=product, product_name=product.name, product_sku=product.sku, unit_price=product.price, quantity=item.quantity, ) ) total += product.price * item.quantity OrderItem.objects.bulk_create(order_items) order.total_amount = total order.save(update_fields=['total_amount']) clear_cart(request.user) return order

这段代码把整个下单链路塞进了一个事务里,任何一个异常都会回滚,保证不会出现“订单生成但库存没减”或者“库存减了但订单没生成”的中间态。注意product.save(update_fields=['stock'])这个写法,它是在告诉Django只更新库存字段,避免把整行数据写回导致并发冲突概率上升。

4.2 并发扣库存:为什么必须用F表达式或行级锁

上面的事务解决了原子性,但没有解决并发问题。两个用户同时下单,各自读到stock=1,各自扣减,最终库存变成0而不是-1已经是运气好,运气不好就是超卖。修正方法是把“扣库存”变成一个原子操作,不再“先读再写”。

from django.db.models import F def deduct_stock(product_id, quantity): updated = Product.objects.filter(id=product_id, stock__gte=quantity).update(stock=F('stock') - quantity) if updated == 0: raise ValueError('库存不足')

这里用update+filter的方式,把“判断库存是否足够”和“扣减”合并成一个在数据库层面执行的原子操作。stock__gte保证只有当前库存大于等于购买数量时才扣减,返回的updated如果为0,就说明抢不到库存,直接抛出异常。事务里先做这个扣减,再创建订单,顺序不能反。

4.3 支付回调的幂等处理:重复通知不是异常,是常态

接入某第三方支付平台后,回调接口收到重复请求是正常现象。平台为了保证通知送达,会多次发送异步通知,应用端必须保证重复通知不产生副作用。实现方式是在订单上加一个状态判断,只有“已创建”状态的订单才能被更新为“已支付”。

from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from django.utils.timezone import now @csrf_exempt def payment_callback(request): # 验签逻辑略去,核心是状态机幂等 order_id = request.POST.get('order_id') order = Order.objects.select_for_update().get(order_number=order_id) if order.status == Order.Status.PAID: return JsonResponse({'code': 'SUCCESS'}) order.status = Order.Status.PAID order.paid_at = now() order.save(update_fields=['status', 'paid_at']) return JsonResponse({'code': 'SUCCESS'})

注意这里用了select_for_update,把订单行锁住,避免两个回调同时进来时都判断成“未支付”导致重复发货。验签部分一定要放在业务逻辑之前,用支付平台给的密钥做HMAC校验,验签失败直接返回错误码,不要进入任何业务分支。

5. 电商Django项目避坑:5个让人反复翻车的真实问题

这一章专门收集开发和上线阶段反复出现的坑,每条都按现象、原因、解决办法来写。

5.1 现象:POST请求全部返回403,表单什么也没提交成功

原因:Django默认开启CSRF中间件,每个POST请求都必须携带csrfmiddlewaretoken。很多初学者在模板里渲染了表单但没有在form标签里加{% csrf_token %},或者用了fetch/Ajax提交但没在Header里带上X-CSRFToken。解决:模板渲染时在表单内加上{% csrf_token %};如果是前后端分离的接口调用,在前端JS里从cookie读取csrftoken并写到请求头。

<form method="post" action="{% url 'cart:checkout' %}"> {% csrf_token %} <input type="submit" value="提交订单"> </form>

这个坑的表现是“页面能看到但一提交就报错”,排查时优先看浏览器开发工具里请求体是否包含csrfmiddlewaretoken字段。

5.2 现象:库存显示还有货,下单却提示库存不足

原因:数据库里的库存数和页面展示的库存数之间存在缓存,或者下单前的库存校验绕过了原子扣减逻辑。解决:页面上的库存只做展示,真正的校验以第4章deduct_stock的原子操作为准。另外如果用了Redis缓存商品库存,下单扣减时也要同步更新Redis里的值,否则会出现前端反复刷新后看到“幽灵库存”。

5.3 现象:用户支付成功,订单状态还是“已创建”,售前售后全乱

原因:支付回调的验签没做,或者回调处理逻辑在事务外。最常见的情况是回调接口里用了外部的HTTP请求去查询订单状态,这个请求超时导致整个回调处理函数抛出异常,状态没更新。解决:回调里不发起任何外部网络请求,只做验签、查库、更新状态三件事。确实需要查第三方API的,可以放到Celery任务里异步执行。

5.4 现象:图片上传成功,但商品详情页图片404

原因:MEDIA_ROOT和MEDIA_URL配置不完整,又或者Nginx没有把upload目录映射到URL。Django开发环境跑runserver时图片正常,一到生产环境就404,是典型的部署配置遗漏。解决:settings.py里设置MEDIA_ROOT和MEDIA_URL,生产环境在Nginx里增加location映射;如果坚持用Django处理媒体文件,记得在urls.py里加static()帮助函数,但生产环境强烈建议交给Nginx。

5.5 现象:促销活动开始后,数据库CPU打满,接口全部超时

原因:库存扣减的update语句在高并发下争抢同一行锁,同时订单创建逻辑在事务里执行了太多非必要的查询。解决:把扣库存操作前移,独立成一个只有一条update语句的短事务,成功后再进入创建订单的长事务;同一个商品SKU的扣减操作落在单独的Redis队列里串行执行,可以有效缓解行锁竞争。

6. 性能优化与安全加固:上线前最后一道工序

功能都做完了,接下来是把Django项目的性能和安全调整到可以上线接受真实流量的状态。

6.1 缓存策略:Django到底该缓存什么

电商站点里,商品列表页、分类页、首页流量占比最高,而这些页面的数据变动相对低频。我一般会做一个中间件级别的页面缓存,按URL和查询参数生成缓存键,TTL设为300秒。购物车和订单属于用户私有数据,绝对不能做全局缓存。会话数据建议从数据库搬进Redis,Django配置如下。

# settings.py CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'CONNECTION_POOL_KWARGS': {'max_connections': 50}, }, } } SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_CACHE_ALIAS = 'default'

SESSION_ENGINE切到cache后,所有会话读取都走Redis,写操作也直接回写。这里的坑是Redis连接池默认值太小,高并发下会报连接超时,max_connections调到50只是起步值,要根据实际QPS调整。

6.2 安全配置回滚检查清单

上线前我会逐项检查Django的SECRET_KEY是否泄漏、DEBUG是否为False、ALLOWED_HOSTS是否包含了线上域名。支付回调接口的CSRF装饰器要单独确认是否已经豁免并做好验签。Django 4之后默认的密码哈希算法是PBKDF2,足够安全,不需要额外调整。如果条件允许,全站开启HTTPS,并在settings里加上SECURE_SSL_REDIRECT和SECURE_HSTS_SECONDS。这些配置单独看没什么技术含量,但漏掉任何一项都可能在真实流量下暴露问题。

Django做的电商项目,开发阶段最容易被忽略的就是“数据可追溯性”和“并发正确性”这两件事。我早期做过一个商城Demo,订单状态只允许创建和完成两种,结果促销期间出现用户重复支付、重复发货的乱子,整个排查过程非常痛苦,最后把状态机细化、加上回调幂等才彻底解决。现在每次规划新的电商项目,我都会先在纸上画一遍订单状态流转,把事务边界标出来再动代码。这个习惯帮我节省了大量返工时间。希望这些经验能帮你把Django电商项目的坑提前踩平,少走一些弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 3:49:07

深入理解Linux文件描述符:系统调用、重定向与缓冲区机制

不管你是刚学 Linux 系统编程&#xff0c;还是写 C/C 写了好一阵子却总在printf和write之间犯迷糊&#xff0c;文件描述符&#xff08;fd&#xff09;都是一道绕不过去的坎。我们每天都在用ls > out.txt把命令结果存进文件&#xff0c;也见过2>&1把报错信息一起重定向…

作者头像 李华
网站建设 2026/10/13 3:48:40

基于SpringBoot+Vue的校园招聘网站实战:从架构设计到部署全解析

校园招聘网站这个题目&#xff0c;在毕业设计和课程项目里出现的频率真不低。但大多数网上的资料只是把代码堆上去&#xff0c;讲不清为什么这样设计&#xff0c;更别提调试阶段那些踩坑记录了。这次就以一个基于SpringBoot Vue的校园招聘网站为例&#xff0c;把项目从架构设计…

作者头像 李华
网站建设 2026/10/13 3:48:25

Gemini CLI 配置指南:settings.json、环境变量与命令行参数全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 3:47:52

计算机系统基础:从五大部件到存储程序,彻底搞懂机器底层

经常有初学的朋友跑来问我&#xff1a;计算机系统基础里“基本组成”这一章&#xff0c;是不是把输入设备、输出设备、存储器、运算器、控制器这张图背熟就完事了&#xff1f;如果只是为了应付期末考试&#xff0c;背图也许够了&#xff1b;但你要是真的想知道“一段代码写出来…

作者头像 李华
网站建设 2026/10/13 3:47:15

C盘又满了?从空间分析到安全清理,释放十几个G的完整方案

C盘又飘红了&#xff0c;这次只剩不到10个G&#xff0c;打开软件都开始转圈圈。多数人的第一反应是进C盘手动翻文件夹&#xff0c;看到可疑的目录就删&#xff0c;删完往往更糟&#xff1a;要么启动变慢&#xff0c;要么系统组件坏掉。用对工具&#xff0c;摸清空间去向&#x…

作者头像 李华
网站建设 2026/10/13 3:47:00

微服务骨架搭建实践:从服务拆分到Nacos注册与Feign调用的关键问题

day-06 是我真正把微服务骨架搭通的一天。前五天一直在看概念、画架构图&#xff0c;第六天终于有四个服务注册到了同一个注册中心&#xff0c;A 服务能通过服务名调用 B 服务并正常返回数据。那种感觉就像是把一堆零件组装成了能发动的机器&#xff0c;虽然还不太稳&#xff0…

作者头像 李华