简介一个基于Django框架的完整电商项目源码面向Python Web开发入门与进阶人群覆盖商家入驻、商品管理、会员注册、手机验证、购物车操作、订单结算、模拟支付以及省市区街道四级联动地址等核心功能可帮助读者理解电商系统从商品展示到支付下单的完整业务流程。资源包为RAR压缩格式共包含561个文件大小约96.63MB。文件类型以图像资源为主包括259个JPG和90个PNG图片用于商品展示、轮播图、界面设计等后端代码方面有70个PY脚本和55个PYC文件实现模型、视图、表单与业务逻辑前端配套37个JS、18个HTML、16个CSS文件构成页面交互与样式。另有少量JSON、SVG、字体文件及地图文件整体结构较完整。目前已有1773人浏览学习属于Django电商类源码中关注度较高的资源。下载后可获得可直接运行的完整项目通过阅读模型、视图、模板的调用关系可梳理账号、商品、购物车、订单等模块的数据流同时借助附带的商品图片与界面素材可在源码基础上进行二次开发快速搭建一套可用于毕设或个人练习的电商演示系统。1. Django完整版电商为什么说业务逻辑不难“完整”二字才吃功夫用Django做电商商品表和分类表的增删改查半天就能写完真正让人在工位上挠头的是订单状态机没约束住、库存扣减并发超卖、支付回调重复处理。所谓完整版指的不是页面多、代码量大而是业务闭环完整商品能上架、用户能加购、订单能下单、库存能扣住、支付能回调、后台能管理这条链路每个环节都得能自圆其说。这篇笔记就按这个闭环拆开讲表结构怎么定、骨架怎么搭、订单库存支付这三块硬骨头怎么啃以及我踩过的五个坑。适合有 Django 基础、想自己做商城后端或者小团队要评估 Django 电商可行性的开发者。2. 从需求到模块一个完整版电商需要拆出哪 12 张核心表很多项目翻车不是翻在代码上而是翻在表结构反复改。运营说“加个规格”你说“改表”财务说“要看优惠分摊”你说“加字段”。所以说完整版电商的第一步是把核心表一次性拆到位。我按常见业务域拆出 12 张表商品域 4 张分类、SPU、SKU、品牌用户域 2 张自定义用户、收货地址交易域 4 张购物车、订单主表、订单明细、支付记录再加上后台权限组的关联表和退款记录表。下面逐个说设计理由。2.1 商品域设计SPU/SKU分离分类树与搜索索引商品域首先要把 SPU 和 SKU 拆开。SPU 是“商品链接”SKU 是“具体可卖的东西”。一件 T 恤的标题、详情、主图是 SPU 的事黑/白两个颜色、S/M/L 三个尺码组合出 6 个库存、6 个价格这是 SKU 的事。如果不拆开每个规格都要重复存一遍标题和详情数据冗余还在其次真正麻烦的是后台上架时改一次标题要扫全表。# goods/models.py from django.db import models class Category(models.Model): name models.CharField(分类名, max_length64) parent models.ForeignKey(self, nullTrue, blankTrue, related_namechildren, on_deletemodels.CASCADE) path models.CharField(路径, max_length255, blankTrue, editableFalse) def save(self, *args, **kwargs): super().save(*args, **kwargs) self.path f{self.parent.path}/{self.id} if self.parent else f/{self.id} super().save(*args, **kwargs) class Meta: db_table goods_category verbose_name 商品分类 class SPU(models.Model): title models.CharField(商品标题, max_length128) category models.ForeignKey(Category, related_namespus, on_deletemodels.PROTECT) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table goods_spu class SKU(models.Model): spu models.ForeignKey(SPU, related_nameskus, on_deletemodels.CASCADE) spec_snapshot models.JSONField(规格快照, defaultdict) price models.DecimalField(售价, max_digits10, decimal_places2) stock models.PositiveIntegerField(库存, default0) sales models.PositiveIntegerField(销量, default0) is_on_sale models.BooleanField(是否上架, defaultTrue) class Meta: db_table goods_sku indexes [ models.Index(fields[spu, is_on_sale]), ]逻辑说明分类表用自关联外键再维护一个 path 字段作用是把“查某分类下所有子分类”从递归变成前缀查询path__startswith父类路径一条 SQL 就能取完这比每次递归子节点再拼 OR 条件可靠也比直接盲目上 mptt 树更容易排查。SPU 的分类外键用on_deletemodels.PROTECT防止商品引用中的分类被直接删掉产生孤儿数据。SKU 里的spec_snapshot是下单时规格快照用的记录“黑 / M / 99 元”这套原始描述即使商品后来改了规格历史订单仍然能看到当时买的到底是什么。搜索方面起步阶段不需要搞 Elasticsearch。商品量在几万条以内Django ORM 的spu__title__icontains加一个普通索引就能应付到分页变慢、后台搜索超 2 秒再考虑上全文检索或者独立搜索服务。一上来就上搜索中间件维护成本和数据同步都够喝一壶。2.2 交易域设计购物车、订单、支付与回调的取舍购物车我建议登录用户存表、游客存会话。登录用户存表的好处是可以跨设备同步后端也好统计加购转化率游客购物车放 session 里等用户登录时再合并。注意合并逻辑有一个坑游客车和用户车里的同一 SKU数量要累加而不是覆盖否则用户加购的 2 件会在登录后悄悄变成 1 件。订单拆主表和明细表是完整版电商绕不开的选择。主表存订单号、总金额、状态、用户明细表存每一个 SKU 的下单快照、成交单价、数量。拆开的原因很简单一个订单可能有多个商品如果不拆主表就要存一个 JSON 数组或者逗号分隔的字符串后续对账、退款、售后处理每一行都得解析一遍拆开之后退款按明细行退、报表按明细行聚合都是舒服的关联查询。支付回调更不能直接在订单模型上加字段了事。第三方支付平台回调同一笔订单可能触发多次每次回调的原始报文都要存下来否则排查对账问题的时候一点痕迹都没有。# trade/models.py from django.db import models from django.conf import settings class Cart(models.Model): user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namecart_items) sku models.ForeignKey(goods.SKU, on_deletemodels.CASCADE) quantity models.PositiveIntegerField(数量, default1) selected models.BooleanField(是否勾选, defaultTrue) class Meta: db_table trade_cart unique_together (user, sku) class Order(models.Model): order_no models.CharField(订单号, max_length64, uniqueTrue) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_nameorders) status models.CharField(状态, max_length16, defaultpending) total_amount models.DecimalField(总金额, max_digits12, decimal_places2) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table trade_order class OrderItem(models.Model): order models.ForeignKey(Order, related_nameitems, on_deletemodels.CASCADE) sku models.ForeignKey(goods.SKU, on_deletemodels.PROTECT) sku_snapshot models.JSONField(下单时的SKU快照, defaultdict) price models.DecimalField(成交单价, max_digits10, decimal_places2) quantity models.PositiveIntegerField(数量) class Meta: db_table trade_order_item class Payment(models.Model): order models.ForeignKey(Order, related_namepayments, on_deletemodels.CASCADE) out_trade_no models.CharField(商户单号, max_length64) provider_trade_no models.CharField(支付平台单号, max_length64, blankTrue) payload models.JSONField(回调原始报文, defaultdict) state models.CharField(处理状态, max_length16, defaultpending) class Meta: db_table pay_payment unique_together (order, out_trade_no, provider_trade_no)逻辑说明订单用户外键用PROTECT意思是用户可以被禁用但不能被删掉否则历史订单会悬空。sku_snapshot与spec_snapshot同样思想订单明细里存的是下单那一刻的价格和规格描述不是实时去 JOIN 商品表避免商品改价后历史订单金额说不清。Payment 表是“支付动作流水”同一订单的多次支付尝试、多次回调各占一行unique_together用订单、商户单号、支付平台单号作联合唯一键直接天然挡掉大部分重复回调。2.3 用Django自带Admin做后台还是重写管理端后台管理是完整版电商容易被低估的一块。很多项目把精力全放在前台接口上最后运营提了一堆后台需求才意识到管理端做得浅了。我的判断标准是这样SKU 数量在几千量级、运营操作就是常规上下架和改价、没有审批流和多级权限用 Django Admin 加上自定义 Action 足够一旦出现“运营提交上架申请→主管审批→商品可见”这类流程或者要按品牌维度做数据权限隔离再重写管理端。起步阶段建议把精力留给核心交易链路后台用 Admin 先顶上。下面是一个可用的 SKU 管理配置十分钟就能接进项目。# goods/admin.py from django.contrib import admin from .models import SKU admin.action(description批量下架选中SKU) def off_shelf(modeladmin, request, queryset): queryset.update(is_on_saleFalse) admin.register(SKU) class SKUAdmin(admin.ModelAdmin): list_display (id, spu, price, stock, is_on_sale) list_filter (is_on_sale,) search_fields (spu__title,) actions (off_shelf,) list_per_page 50逻辑说明list_display决定列表页展示列list_filter给常用筛选项search_fields配置搜索框可以搜 SPU 标题字段actions注册批量操作list_per_page控制分页行数避免一次渲染几千行卡死后台页面。这里强调一次取舍后台 Admin 是给内部运营用的性能要求比前台低得多不要在 Admin 列表里给每个 SKU 再加一堆关联查询字段哪怕你select_related用得很熟练运营点开列表页会等你两秒体验也很差。3. 搭建最小可运行的Django电商骨架环境、模型与迁移表结构定完之后下一步是搭骨架。我建议从零初始化项目不要复制网上的大而全模板因为那些模板里往往带了作者自定义的中间件、第三方依赖和一堆你暂时用不上的代码排查问题时多一个黑匣子就多一分成本。下面这套结构我反复用过多次适用于商品、交易、用户三个核心域都已规划好的场景。3.1 项目结构划分单一Django项目还是多App拆分一个 App 写到底只适合学习 Demo做成完整版电商至少要按业务域拆成独立 App。拆的好处第一是边界清楚trade 里不 import goods 的模型那不可能但至少不允许 trade 反向去查询 users 的表结构第二是迁移文件互不干扰多人开发时不会因为都在同一个 models.py 里改导致迁移冲突。我常用的目录结构如下。my_shop/ ├── manage.py ├── config/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── users/ │ ├── goods/ │ ├── trade/ │ ├── pay/ │ └── rbac/ ├── common/ │ ├── utils.py │ └── cache.py └── scripts/ └── smoke.py逻辑说明apps/目录下每个子目录是一个 Django Appcommon/放所有 App 共用的工具函数比如生成订单号、统一返回格式、缓存 key 管理scripts/放运维脚本和冒烟测试脚本不参与启动。注意apps这个目录并不是 Django 强制要求只是让项目根目录更干净。如果团队里有人对 Django 的 App 路径不熟记得在settings.py里把INSTALLED_APPS写成apps.users这种带前缀的路径并在每个 App 目录下准备apps.py配置name apps.users。3.2 核心模型代码与迁移命令自定义用户与settings配置完整版电商第一步就应该自定义用户模型不要等建完库再改。Django 默认的 User 模型没有手机号字段而国内电商场景手机号是登录账号的一等公民后期换自定义模型会牵一发动全身。先把用户模型定义好再配置AUTH_USER_MODEL然后才做第一次迁移。# users/models.py from django.contrib.auth.models import AbstractUser class User(AbstractUser): mobile models.CharField(手机号, max_length11, uniqueTrue) avatar models.URLField(头像, blankTrue) class Meta: db_table users_user# config/settings.py AUTH_USER_MODEL users.User INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, apps.users, apps.goods, apps.trade, apps.pay, ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: my_shop, USER: shop, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }cd my_shop python manage.py makemigrations users goods trade pay python manage.py migrate逻辑说明设置AUTH_USER_MODEL必须在第一次migrate之前因为 Django 的权限表、会话表都跟这个模型绑定后面再改迁移文件会乱成一团大概率只能重建测试库。数据库字符集用utf8mb4电商的商品标题里随时可能带 emoji 表情默认utf8会直接报Incorrect string value。MySQL 版本建议 8.x 及以上事务隔离级别和窗口函数支持都比 5.7 舒服。迁移命令里makemigrations只负责生成迁移文件不落库真正建表靠migrate每次改模型之后先跑makemigrations再migrate养成习惯就不会出现模型和数据库对不上。提示不要把SECRET_KEY和数据库密码写进 settings.py 提交到代码仓库。用环境变量读取本地开发给个默认值即可。3.3 用DRF暴露API还是用模板渲染页面的选型骨架阶段还要定一件事接口方案。当前主流是前后端分离Django 只提供 REST API前端用 Vue 或 React 单页应用另一条路是 Django 模板渲染服务端页面适合纯 PC 端、团队以后端为主、不打算做小程序和 App 的场景。如果你要做的完整版电商需要同时服务 H5、小程序、将来的 App直接用 DRF 写 API 是唯一不后悔的选择否则模板渲染能把开发周期砍掉三分之一。# goods/views.py from rest_framework import viewsets from rest_framework.decorators import action from .models import SKU from .serializers import SKUSerializer class SKUViewSet(viewsets.ReadOnlyModelViewSet): queryset SKU.objects.filter(is_on_saleTrue) \ .select_related(spu) \ .prefetch_related(spu__skus) serializer_class SKUSerializer逻辑说明用ReadOnlyModelViewSet而不是ModelViewSet商品列表不需要写接口防止开放了不该有的写入口。select_related(spu)解决 SKU 反查所属商品详情时的单次 JOINprefetch_related(spu__skus)解决列表页要展示同一 SPU 下其他规格时最容易踩的 N1 问题。这两行代码对商品列表接口的响应速度影响是数量级的后面避坑章节会专门展开。4. 订单与库存的关键实现扣减时机、幂等与状态机电商后端真正值钱的部分不在商品 CRUD而在订单和库存的并发控制。下面这三块是完整版电商的硬骨头库存什么时候扣、订单状态怎么流转、支付回调怎么做到不重不漏。三者环环相扣处理不好就是线上事故。4.1 库存扣减预占库存与支付后扣减怎么选初学者最常见的选择是“下单就扣库存”理由朴素不扣库存超卖怎么办但下单扣库存会带来另一个问题用户提交订单后不支付库存一直被锁着半小时后这单超时释放期间其他用户想买都买不了。只支付后扣库存更危险两个人同时下单同一件商品支付时库存只剩一件后支付的那个人钱付了却没有货这是真正的灾难。常见做法是“预占库存”下单时在事务里对 SKU 行加锁扣减可售库存同时给订单打上待支付标记订单超时未支付则回补库存。预占方案既挡住超卖又不会让库存被无效订单锁太久。# trade/service.py from django.db import transaction from django.db.models import F from .models import Order, OrderItem transaction.atomic def create_order(user, sku_id, qty): sku SKU.objects.select_for_update().get(idsku_id) if sku.stock qty: raise ValueError(库存不足) sku.stock F(stock) - qty sku.save(update_fields[stock]) order Order.objects.create( order_nogenerate_order_no(), useruser, total_amountsku.price * qty, statuspending, ) OrderItem.objects.create( orderorder, skusku, sku_snapshotsku.spec_snapshot, pricesku.price, quantityqty, ) return order逻辑说明select_for_update()是核心它对满足条件的行加排他锁直到事务结束。同一个 SKU 的并发下单会被排队后到的请求等锁释放后再读库存不会出现两个事务同时读到 stock1 然后都扣成 0。F(stock)做原子自减避免读改写之间的竞态。update_fields[stock]只更新库存字段减少不必要的写操作。如果对这套组合的并发能力没把握记得把数据库隔离级别设为 Read CommittedMySQL 的默认级别就是它PostgreSQL 也是默认都能配合行锁正常工作。另一个常见方案是乐观锁UPDATE goods_sku SET stock stock - 1 WHERE id ? AND stock 1受影响行数为 0 说明库存不足。这个方式胜在无锁、快但下单和扣库存拆成两步之后需要在代码里额外处理“扣成功但订单创建失败”的补偿逻辑。我的建议是起步阶段用悲观锁select_for_update数据量上来之后再考虑拆分库存服务和乐观锁别一开始就把自己逼到补偿事务的复杂度里。4.2 订单状态机的定义与流转约束订单状态不能散落在代码各处。常见反模式是每个视图函数里自己写if order.status pending结果订单状态越改越多最后没人说得清哪些状态能跳转到哪些状态。把状态定义成枚举然后把允许的流转集中在一个表里是减少线上故障的最有效手段。# trade/constants.py from django.db import models class OrderStatus(models.TextChoices): PENDING pending, 待支付 PAID paid, 已支付 SHIPPED shipped, 已发货 COMPLETED completed, 已完成 CANCELLED cancelled, 已取消 REFUNDING refunding, 退款中 TRANSITIONS { OrderStatus.PENDING: {OrderStatus.PAID, OrderStatus.CANCELLED}, OrderStatus.PAID: {OrderStatus.SHIPPED, OrderStatus.REFUNDING}, OrderStatus.SHIPPED: {OrderStatus.COMPLETED, OrderStatus.REFUNDING}, OrderStatus.REFUNDING: {OrderStatus.CANCELLED}, } def change_status(order, new_status): allowed TRANSITIONS.get(order.status, set()) if new_status not in allowed: raise ValueError(f{order.status} 不能流转到 {new_status}) order.status new_status order.save(update_fields[status])逻辑说明TRANSITIONS字典定义了每一条合法流转路径例如待支付订单可以流转到已支付或已取消但不能直接跳到已发货已支付订单可以发货或进入退款不能回到待支付。change_status是唯一的改状态入口任何人修改订单状态都走这个函数非法流转直接抛异常。这样设计的好处是业务方说“我们要支持取消已发货订单”你只需要评估在状态机里加一条SHIPPED - CANCELLED的流转而不是把十几个视图里的 if 都翻一遍。参数里注意update_fields[status]避免把整行数据覆盖写一遍。4.3 支付回调的幂等处理与对账支付回调是完整版电商最容易出线上事故的一段。回调可能延迟、可能重复、可能在订单已取消之后才到如果不做幂等处理重复回调会把订单状态重刷多次甚至把已退款的订单重新置为已支付。我见过最严重的翻车现场就是回调处理函数没有去重同一笔支付触发三次回调订单金额、状态、库存全乱套。幂等处理的核心在于用一个联合唯一约束顶住重复请求再用状态机限制非法流转。# pay/views.py import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from apps.trade.models import Order, Payment from apps.trade.constants import OrderStatus, change_status csrf_exempt def payment_callback(request): payload json.loads(request.body) # 验签逻辑按支付平台文档实现建议抽成独立函数 if not verify_signature(payload): return JsonResponse({code: FAIL}) order_no payload.get(out_trade_no) provider_no payload.get(trade_no) payment, created Payment.objects.get_or_create( order__order_noorder_no, provider_trade_noprovider_no, defaults{payload: payload}, ) if not created and payment.state success: return JsonResponse({code: SUCCESS}) order Order.objects.get(order_noorder_no) if order.status OrderStatus.PENDING: change_status(order, OrderStatus.PAID) payment.state success payment.save(update_fields[state]) return JsonResponse({code: SUCCESS})逻辑说明get_or_create以order_no provider_trade_no为幂等键第一次回调创建 Payment 记录第二次回调拿不到 created 就直接返回成功不会再改订单。verify_signature是接口里最不能省的一步回调里带的金额和单号都要参与验签防止伪造回调。处理订单状态前还要判断当前是不是pending如果订单已经取消不能因为回调到了就强行支付成功——这就是上一节状态机在真实场景里的价值。返回给支付平台的SUCCESS字符串必须遵守平台协议否则平台会认为回调失败进入重发循环。注意csrf_exempt是必须的支付平台服务器没有你的 CSRF Cookie。但一定要配合验签逻辑否则等于把支付回调接口裸奔在公网上。另外把回调接口放在独立 App 里便于后期做访问 IP 白名单和单独日志。5. Django电商避坑记录5个翻车现场与修复方案完整版电商开发周期长翻车点集中发生在迁移管理、并发扣减、查询性能和模型设计这几个地方。下面五条是我在多个项目里反复踩过的坑每一条都按现象、原因、解决三步写清楚照着排查能省下大量查资料时间。5.1 现象migrate 时提示迁移依赖冲突数据库没建出来开发到中途新建了一个 App跑python manage.py migrate时 Django 提示Migration apps.xxx is applied before its dependency或者Conflicting migrations detected。尤其是多人协作的仓库两个分支同时给同一个模型加了字段各自生成了迁移文件合并后依赖顺序对不上。原因就是迁移文件是顺序执行的而每个人都基于不同的迁移历史生成新文件合并后两个迁移都依赖同一个旧迁移节点Django 不知道该先跑哪个。解决办法是优先使用--merge让 Django 自动合并冲突的迁移分支python manage.py makemigrations --merge python manage.py migrate如果--merge还搞不定就检查migrations/目录里__init__.py的依赖声明手动把两个冲突迁移的dependencies指向同一个父节点。给团队定一条规矩模型改动提交前先跑makemigrations并一起提交迁移文件不要在服务器上现场生成迁移能避免九成此类问题。5.2 现象并发下单后库存变负数超卖事故压测时用五十个线程同时下单同一件库存为 10 的商品最终数据库里 stock 变成了负数。代码里明明写了if sku.stock qty判断。原因有两个可能一是没有把查询和扣减放在同一个事务里先查库存、再扣减中间隔了好几步并发时两个请求都读到 stock10二是用了select_for_update但事务没有真正包住整个操作比如在自动提交模式下执行了select_for_update锁随查询结束就释放了。解决方法是把扣减逻辑收敛到一个事务函数里用transaction.atomic和select_for_update组合见第 4.1 节。这里强调一个排查技巧在 MySQL 里打开事务日志观察select_for_update是否真的发出了FOR UPDATE语法如果 DDL 和 DML 混在事务里锁的持有时间会远超预期但至少不会超卖。5.3 现象商品列表页响应时间超过 3 秒数据库 CPU 打满前台商品列表每页 20 条数据接口却慢到 2-3 秒。打开debug_toolbar的 SQL 面板发现执行了 100 多条查询。原因就是典型的 Django N1 查询列表页遍历 SKU 时每一条都要反查一次 SPU 标题、再查一次规格快照、再查一次分类名20 条数据就产生 20 次关联查询热门接口在并发时直接把数据库打挂。解决是用select_related和prefetch_related把关联查询合并成几条参考第 3.3 节的视图代码。另一个习惯是给所有列表接口写一个测试断言查询数量不超过 10 条。from django.test import TestCase from django.test.utils import CaptureQueriesContext from django.db import connection class SKUListQueryCountTest(TestCase): def test_list_api_query_count(self): with CaptureQueriesContext(connection) as ctx: response self.client.get(/api/skus/) self.assertLess(len(ctx.captured_queries), 10)这个断言能自动防止新来的同事在视图里加一个不加预取的关联字段比 code review 靠肉眼抓 N1 可靠得多。5.4 现象CBV 视图继承层级太深改一个功能要翻五个文件早期图省事把商品列表写成SKUListView(ListAPIView)后来加筛选变成SKUFilterView(SKUListView)再加搜索变成SKUSearchView(SKUFilterView)最后改一个字段要逐层翻父类代码才知道数据从哪里来。原因是对 CBV 的滥用。CBV 适合小范围的类复用一旦继承层级超过三层代码的可读性和可追踪性直线下降排查问题时它在 Django 源码的黑匣子里层层绕。解决我之前常用的做法是超过三层复用就果断改成函数视图或 DRF ViewSet。DRF 的ViewSet已经封装了 list/retrieve/action 的标准流程自定义逻辑写在get_queryset或action方法里层次清晰。记住一条视图层追求“一眼看穿”不要追求“复用最大化”。5.5 现象AUTH_USER_MODEL 配置太晚数据库重建才能收场项目开发了半个月订单表、支付表都建好了领导说用户表要加手机号登录这时候才想起来自定义 User 模型。配好AUTH_USER_MODEL后一跑迁移报一堆外键依赖错误。原因很简单Django 的auth_user表已经通过内置的auth迁移创建出来了且Order.user、Cart.user这些外键都指向了旧模型中途换新模型Django 无法自动重挂这些外键。解决这类问题的后悔药分两种。开发阶段最干脆直接删掉users/migrations/下所有迁移关掉测试库重建重新migrate。已上线数据的项目则要分步走先建新用户表、迁移数据、再改外键关系过程非常痛苦。所以新项目第一次migrate之前务必先配好AUTH_USER_MODEL如果你还没有开始写业务模型现在就停下来把自定义用户模型配上再继续。6. 完整版电商的验证清单与上线前的性能压测写到这里代码层面的闭环基本成型商品能上架、购物车能加、订单能下、库存能扣、回调能处理。但完整版的价值要上线后才知道上线前的验证和压测不能省。下面是我每次交付前必做的三件事。6.1 一台4核8G服务器能扛多少并发压测口径与结果一台 4 核 8G 的云主机跑 Django MySQL不做读写分离、不开缓存优化的前提下商品列表这类只读接口经压测常见结果在 300-500 QPS 之间下单接口因为涉及行锁和事务通常掉到 50-100 QPS。这个数字不是玄学而是事务开销带来的必然差异下单请求平均耗时几十毫秒加上锁等待和提交吞吐自然上不去。压测用 wrk 或 Locust 都可以Locust 写 Python 脚本更适合模拟商城的带登录态操作。# scripts/locustfile.py from locust import HttpUser, task, between class ShopUser(HttpUser): wait_time between(1, 3) def on_start(self): self.client.post(/api/users/login/, json{mobile: 13800000000, password: test123}) task(5) def sku_list(self): self.client.get(/api/goods/skus/?page1) task(1) def create_order(self): self.client.post(/api/trade/orders/, json{sku_id: 1, quantity: 1})逻辑说明给列表接口和下单接口分配不同权重贴近真实用户行为on_start每次压测会话先登录拿到会话后模拟加购下单。压测时不要盯着 QPS 绝对值重点看错误率和 P95 响应时间下单接口错误率超过 1% 就说明事务参数没调好。6.2 用限流和异步任务给支付回调留出余量压测会发现订单创建接口是瓶颈支付回调的验签和落库同样吃资源。给回调接口加 DRF 的限流防止支付平台重发风暴或恶意请求把应用打挂# config/settings.py REST_FRAMEWORK { DEFAULT_THROTTLE_RATES: { payment_callback: 30/minute, } }# pay/views.py from rest_framework.throttling import ScopedRateThrottle class PaymentCallbackView(APIView): throttle_classes [ScopedRateThrottle] throttle_scope payment_callback逻辑说明ScopedRateThrottle配合DEFAULT_THROTTLE_RATES里的scope值按分钟做计数支付平台正常回调一分钟不会超过几次30/分钟已经是宽松余量。回调处理里所有非核心动作比如发送消息通知、更新库存聚合应该丢到 Celery 异步任务里前端用户不关心这些耗时操作。落库顺序保持“验签→幂等判断→更新状态→返回成功”不变异步只处理那些失败也不影响主流程的旁路逻辑。6.3 一个习惯每次交付前跑的冒烟脚本上线前一晚别只盯着压测报告跑一遍真实用户链路比什么都重要。我习惯写一个冒烟脚本模拟注册登录、浏览商品、加入购物车、下单、模拟支付回调、确认订单状态这六步任何一步报错直接终止。# scripts/smoke.py import requests BASE http://127.0.0.1:8000 def test_core_flow(): session requests.Session() assert session.post(f{BASE}/api/users/register/, json{mobile: 13900000001, password: x} ).status_code 201 assert session.post(f{BASE}/api/users/login/, json{mobile: 13900000001, password: x} ).status_code 200 r session.get(f{BASE}/api/goods/skus/?page1) assert r.status_code 200 sku_id r.json()[results][0][id] assert session.post(f{BASE}/api/trade/orders/, json{sku_id: sku_id, quantity: 1}).status_code 201 print(冒烟通过核心链路无异常) if __name__ __main__: test_core_flow()逻辑说明注册、登录、列表、下单每一步都返回值校验任何一个非预期状态码都让脚本退出。冒烟脚本只验证核心链路能跑通不验证细节它存在的意义是防止“改了一个配置整个交易链路全挂”这种情况发生。我最后一次翻车就是改了个 Redis 缓存 key 前缀导致订单创建读取商品信息时全部缓存击穿数据库连接被打满冒烟脚本当场拦住没让带着问题的版本上线。从那以后每次改动涉及缓存、数据库、配置里的任何一项我都会先把这个脚本跑一遍再谈别的。希望这套习惯对你也有用祝顺利上线。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?