摘要:
上篇主要介绍了这个 Django 电商后端项目的整体架构、模块划分和核心业务模型。这一篇继续往里拆,重点分析订单提交流程为什么要用事务、订单取消为什么要抽成服务层、Redis 缓存是如何做自动失效的,以及 Celery 是怎么实现订单通知和超时自动取消的。最后再结合测试用例,看看这个项目是怎么把关键链路兜住的。

关键词: Django REST Framework、事务、Redis、Celery、订单系统、缓存失效、自动化测试

一、下篇重点看什么

如果说上篇解决的是“项目是怎么搭起来的”,那下篇重点回答的就是“这个项目为什么这样写”。

电商后端最容易出问题的地方,通常不在商品列表,也不在登录注册,而是在订单链路。因为订单会同时影响多个对象:购物车、商品库存、订单主表、订单明细、异步通知、超时取消。只要中间有一步没处理好,数据就可能不一致。

所以这个项目在实现时,重点做了四件事:

  • 用事务保证订单提交的一致性
  • 用服务层复用订单取消逻辑
  • 用 Redis 缓存优化商品读取
  • 用 Celery 处理异步通知和延迟取消

二、订单提交为什么必须用事务

很多人一开始写订单接口时,会直接按顺序做下面几件事:

  1. 查询购物车
  2. 查询商品库存
  3. 创建订单
  4. 扣减库存
  5. 清空购物车

看起来没有问题,但如果执行到一半报错,比如订单创建成功了,库存却没扣成功,那系统数据就乱了。所以订单提交不能靠“按顺序执行”,而要靠事务把这一整组操作绑在一起。

这个项目里的核心入口就是 OrderSubmitAPIView:

class OrderSubmitAPIView(APIView):

def post(self, request): with transaction.atomic(): pass

虽然这里只展示了骨架,但它背后的核心思路很明确:

  • 先锁定当前用户购物车
  • 再锁定购物车项
  • 再锁定即将扣减库存的商品
  • 校验商品是否存在、是否上架、库存是否充足
  • 创建订单主表和订单明细
  • 批量扣减库存
  • 删除购物车中已提交的商品
  • 在事务提交后,再触发异步任务

这里真正关键的,不只是 transaction.atomic(),还有 select_for_update()。
它的作用可以简单理解成:在当前事务完成前,把相关行锁住,避免并发下单导致库存被重复扣减。

也就是说,这个项目在下单时考虑的不是“能不能提交订单”,而是“多个请求同时下单时还能不能保证数据正确”。

三、为什么异步任务要放到事务提交之后

订单提交成功后,项目还做了两件额外的事:

  • 发送订单创建通知
  • 安排一个延迟任务,用于超时自动取消订单

这里没有直接在事务内部调用任务,而是用了 transaction.on_commit(...)。相关骨架如下:

def enqueue_order_event_email(order_id, event): pass

def schedule_order_auto_cancel(order_id): pass

在订单提交流程里,调用方式大致是:

transaction.on_commit( lambda order_id=order.id: enqueue_order_event_email(order_id, "created") )

transaction.on_commit( lambda order_id=order.id: schedule_order_auto_cancel(order_id) )

为什么要这样写?因为如果事务还没真正提交,订单数据可能还没落库,这时 Celery worker 抢先执行,就可能查不到订单,或者拿到不完整的数据。

所以正确顺序应该是:

  • 先让数据库事务完整提交
  • 再去派发异步任务

这是一种非常典型、也非常实用的后端工程习惯。

四、订单取消为什么要抽到服务层

订单取消这件事,在这个项目里并不只会发生一次。

它至少有两个入口:

  • 用户主动取消订单
  • 系统检测到超时未支付,自动取消订单

如果把取消逻辑直接写死在视图里,那么自动取消就还得再写一遍,代码会重复,而且后续改规则时很容易漏。

所以项目把订单取消抽成了单独的服务函数:

def cancel_order(order, reason=""): pass

这个函数负责几件核心事情:

  • 校验当前订单状态是否允许取消
  • 锁定订单明细和关联商品
  • 把订单商品数量加回库存
  • 更新订单状态为 cancelled
  • 如果有取消原因,则写入备注字段

这样一来,不管是手动取消还是系统自动取消,最终都走同一套逻辑。
这就是服务层存在的意义:把“业务规则”从“接口入口”里拆出来,提升复用性和可维护性。

对应的接口骨架如下:

class OrderCancelAPIView(APIView):

def post(self, request, order_id): pass

五、Celery 是怎么实现超时自动取消的

电商里一个很常见的业务需求就是:订单创建后,如果用户长时间未支付,就自动关闭订单并恢复库存。

这个项目没有用轮询脚本去扫库,而是采用了 Celery 延迟任务的方式。相关任务函数如下:

@shared_task def send_order_event_email(order_id, event): pass

@shared_task def auto_cancel_pending_order(order_id): pass

其中,auto_cancel_pending_order(order_id) 的思路是:

  • 根据订单 ID 查询订单
  • 判断订单当前是否还是 pending
  • 根据 created_at + ORDER_AUTO_CANCEL_SECONDS 计算是否超时
  • 如果超时,就调用 cancel_order(order, reason=...)

调度方式则是:

auto_cancel_pending_order.apply_async( args=[order_id], countdown=settings.ORDER_AUTO_CANCEL_SECONDS, )

这里的设计很巧妙。
它不是等系统每分钟来扫一次数据库,而是在订单创建的那一刻,就提前安排好一个“未来任务”。到了指定时间,Celery 自动执行;如果用户已经支付或者订单已取消,任务里再判断一次状态即可。

这种方式的优点很明显:

  • 逻辑简单
  • 与订单创建动作天然绑定
  • 不需要额外写定时扫描逻辑
  • 对小中型项目非常实用

六、商品缓存为什么不用“硬删 key”,而用版本号失效

商品列表、商品详情、分类列表这些接口,都是典型的高频读接口。如果每次都直接打数据库,后期访问量一上来,压力会很明显。

所以这个项目给商品和分类查询接入了缓存。相关函数骨架如下:

def build_category_cache_key(scope, query_string): pass

def build_product_list_cache_key(scope, query_string): pass

def build_product_detail_cache_key(scope, product_id): pass

def invalidate_category_cache(): pass def invalidate_product_cache(): pass

它的思路不是“更新一个商品时,手动删掉所有可能相关的 key”,因为商品列表的筛选条件很多,key 很分散,几乎删不干净。

这里采用的是“版本号失效”方案:

  • 分类缓存维护一个版本号
  • 商品缓存维护一个版本号
  • 真正的缓存 key 由 版本号 + 查询条件 + scope 组合生成
  • 商品或分类发生变更时,不直接删全部 key,而是把版本号加一

这样一来,旧 key 虽然还在 Redis 里,但已经没人再访问了;新请求会自动命中新版本 key。

这个方案在项目里还配合了 Django 信号完成自动失效:

@receiver(post_save, sender=Category) @receiver(post_delete, sender=Category)

def invalidate_category_related_cache(**kwargs): pass

@receiver(post_save, sender=Product)

@receiver(post_delete, sender=Product)

def invalidate_product_related_cache(**kwargs): pass

这套写法的优势在于:

  • 缓存逻辑和业务查询逻辑解耦
  • 不需要精确维护复杂 key 集合
  • 商品修改后,缓存自动更新策略明确
  • 对列表页和详情页都适用

七、测试为什么很重要

很多练手项目写到最后,接口能跑就结束了。但电商这种有状态业务,如果没有测试,后面一改功能很容易把已有逻辑带崩。

这个项目目前已经覆盖了 20 个测试用例,主要分布在三个模块里:

class UserAPIViewTests(TestCase): def test_register_success(self): pass

def test_login_returns_tokens(self): pass

def test_user_update_updates_profile(self): pass

class ProductAPIViewTests(TestCase):

def test_product_list_filters_for_anonymous_user(self): pass

def test_staff_can_see_inactive_product(self): pass

def test_product_list_cache_invalidates_after_product_update(self): pass

class OrderAPIViewTests(TestCase):

def test_cart_item_create_merges_existing_quantity(self): pass

def test_order_submit_creates_order_deducts_stock_and_clears_cart(self): pass

def test_order_cancel_restores_stock_and_marks_order_cancelled(self): pass

def test_auto_cancel_pending_order_restores_stock_after_timeout(self): pass

这些测试覆盖的不是“接口能不能返回 200”这么简单,而是项目里最关键的业务规则:

  • 登录后能否正确拿到 Token
  • 商品筛选逻辑是否正确
  • 缓存是否在商品更新后失效
  • 购物车重复添加是否会合并数量
  • 下单后库存是否真的扣减
  • 取消订单后库存是否恢复
  • 超时自动取消任务是否真的生效

这类测试的价值,在于它验证的是“业务结果”,而不是“代码执行了没有”。

八、这个项目还有哪些可以继续优化

虽然这个项目已经比较完整,但从工程角度看,后面还可以继续扩展:

  • 接入分页,避免商品列表一次返回过多数据
  • 增加支付回调和支付状态流转
  • 增加管理员商品管理和订单管理接口
  • 增加接口限流和权限分层
  • 增加 CI 流程,把测试自动化跑起来
  • 增加部署方案,形成完整上线链路

也正因为这些扩展空间存在,这个项目很适合作为长期迭代的个人作品。

九、总结

如果说上篇介绍的是项目框架,那么下篇真正体现的是后端设计思维。

这个项目最值得总结的几点是:

  • 订单提交必须放进事务里,不能分散执行
  • 并发下单需要配合行级锁来保证库存一致性
  • 异步任务要等事务提交后再派发
  • 订单取消应该抽成服务层,避免逻辑重复
  • 商品缓存适合用版本号失效,而不是手动删零散 key
  • 电商项目的关键链路一定要有测试兜底

这也是我觉得 Django 电商项目真正有价值的地方。它不只是把接口写出来,而是在一步步逼近真实业务系统的处理方式。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐