news 2026/10/18 15:49:37

Day 10 | 订单系统:从下单到发货的状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Day 10 | 订单系统:从下单到发货的状态机

摘要:今天来啃电商系统里最硬的一块骨头——订单系统。从状态机设计到下单事务,从乐观锁到悲观锁,一篇讲透。订单全流程:待支付 → 已支付 → 已发货 → 已完成,以及两条岔路——已取消和已退款。代码全部可运行。


为什么订单系统是电商的心脏

你想想,一个电商系统,商品展示可以丑一点,搜索可以慢一点,但订单要是出了问题——

  • 用户付了钱,订单没创建:钱没了,货也没了。
  • 库存扣了,订单没生成:商品凭空蒸发。
  • 两个用户同时买最后一件,库存变成 -1:超卖了,老板哭了。

这不是 bug,这是灾难。

我在做商城的时候,订单模块改了整整三版。第一版没有事务,结果测试环境一压就出脏数据;第二版用了悲观锁,高并发直接卡死;第三版才改成现在的乐观锁 + 状态机方案,稳如老狗。

所以订单系统的设计,核心就三个词:状态机、事务、锁。

今天就把这三个词,一个一个拆明白。


原理速览

状态机:订单的"交通规则"

状态机听起来很学术,其实就是一个规则表:

订单当前是什么状态,下一步能变成什么状态。

就像交通规则——红灯只能变绿,绿可以变黄,但你不能从红灯直接变黄灯。订单也是一样,你不能从"待支付"直接跳到"已发货",必须先经过"已支付"。

事务:要么全做,要么全不做

下单这一个动作,其实包含了 5 个子操作:

  1. 扣减库存
  2. 计算优惠
  3. 创建订单
  4. 创建订单项
  5. 清理购物车

这 5 步必须是一个原子操作——要么全部成功,要么全部回滚。

为什么必须原子?看一个真实的回滚场景:

用户下单买 2 件衣服。第 1 步扣库存成功(仓库 -2),第 2 步计算优惠成功(打 8 折),第 3 步创建订单时数据库连接超时了——崩了。如果没有事务保护,库存扣了但订单没建,用户的 2 件衣服就这么凭空消失了。

有了事务,第 3 步失败会触发回滚,第 1 步的库存扣减也会撤销。世界恢复原样,用户可以重新下单。

锁:防止"一货多卖"

库存只剩 1 件,100 个人同时点"下单"——谁该买到?

两种策略:

策略乐观锁悲观锁
核心思路先操作,提交时检查有没有冲突先加锁,操作完再释放
数据库实现WHERE stock >= quantity条件更新SELECT ... FOR UPDATE行锁
优点性能好,不阻塞其他事务绝对不会超卖
缺点冲突时需要重试高并发下排队等待
适用场景日常购物(冲突概率低)秒杀、限时抢购(冲突概率高)

一句话总结:乐观锁是"信任制",悲观锁是"门禁制"。


动手:从状态机到下单事务

状态机设计

完整的状态转换图:

┌─────────┐ │ 待支付 │ (pending) └────┬────┘ ┌─────┴─────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 已支付 │ │ 已取消 │ ← 终态 │ (paid) │ └─────────┘ └────┬─────┘ ┌─┴──────────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 已发货 │ │ 已退款 │ ← 终态 │(shipped) │ └─────────┘ └────┬─────┘ ┌─┴──────────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 已完成 │ │ 已退款 │ ← 终态 └─────────┘ └─────────┘ (终态)

用代码表示就是这张转换表:

当前状态允许转换的目标状态
待支付 (pending)已支付 (paid)、已取消 (cancelled)
已支付 (paid)已发货 (shipped)、已退款 (refunded)
已发货 (shipped)已完成 (completed)、已退款 (refunded)
已完成 (completed)无(终态)
已取消 (cancelled)无(终态)
已退款 (refunded)无(终态)

代码实现非常简洁:

# 状态转换规则表ORDER_STATE_TRANSITIONS={'pending':['paid','cancelled'],'paid':['shipped','refunded'],'shipped':['completed','refunded'],'completed':[],# 终态'cancelled':[],# 终态'refunded':[],# 终态}defcan_transition(from_status,to_status):"""检查状态转换是否合法"""allowed=ORDER_STATE_TRANSITIONS.get(from_status,[])returnto_statusinallowed

执行转换的函数,顺便记录时间节点(核心逻辑):

deftransition_order(order,target_status):"""执行订单状态转换,同时记录时间节点"""current_status=order.status# 1. 校验转换合法性ifnotcan_transition(current_status,target_status):raiseValidationError(f"非法转换:{current_status}→{target_status}")# 2. 更新状态order.status=target_status# 3. 记录时间节点(paid_at / shipped_at / completed_at)now=timezone.now()iftarget_status=='paid':order.paid_at=noweliftarget_status=='shipped':order.shipped_at=noweliftarget_status=='completed':order.completed_at=now order.save()returnorder

状态机的好处:把业务规则从代码里抽出来,变成一张表。新人来了看这张表就知道订单怎么流转,改规则也只改这一张表。这就是"数据驱动"的思想。

下单事务:5 步原子操作

这是整个订单系统最关键的代码。核心框架(关键片段):

fromdjango.dbimporttransactionfromdjango.db.modelsimportFdefcreate_order(user_id,cart_items,address_info,coupon_discount=None):"""创建订单 —— 整个流程在事务中执行,任何一步失败全部回滚"""withtransaction.atomic():# ① 扣库存(乐观锁:条件更新,affected == 0 说明库存不足)foritemincart_items:affected=Goods.objects.filter(id=item['goods_id'],stock__gte=item['quantity'],# 库存 >= 购买数量).update(stock=F('stock')-item['quantity']# F表达式,数据库原子操作)ifaffected==0:raiseValidationError(f"商品{item['goods_id']}库存不足")# ② 计算总价 + 优惠(优惠后不为负)# ③ 创建订单(生成订单号、地址快照、status='pending')# ④ 批量插入订单项(bulk_create 性能更好)# ⑤ 清理购物车returnorder

为什么要用transaction.atomic()?

想象一下,如果没有事务:

第 1 步:扣库存 ✅ (仓库 -2) 第 2 步:算优惠 ✅ (打 8 折) 第 3 步:建订单 ❌ (数据库连接超时了) 第 4 步:没执行 第 5 步:没执行

结果:库存扣了,优惠算了,但订单没建出来。用户的库存没了,订单也没了。这叫什么?这叫事故。

有了transaction.atomic(),第 3 步失败后,第 1 步的扣库存也会自动回滚。世界恢复原样,用户可以重新下单。

注意F('stock') - quantity的用法。这不是 Python 层面的减法,而是让数据库自己算。F表达式会生成 SQL 的SET stock = stock - N,在数据库层面完成原子操作,避免"先读后写"的竞态条件。

乐观锁 vs 悲观锁

来看两种锁的实际代码对比。

乐观锁(推荐日常使用):

# 核心:WHERE stock >= quantity,条件更新affected=Goods.objects.filter(id=goods_id,stock__gte=quantity,# 条件检查).update(stock=F('stock')-quantity# 数据库层面原子扣减)ifaffected==0:raiseValidationError("库存不足")

悲观锁(秒杀场景):

# 核心:select_for_update() 加行锁goods=Goods.objects.select_for_update().get(id=goods_id)ifgoods.stock<quantity:raiseValidationError("库存不足")goods.stock-=quantity goods.save()

两者区别在哪?

乐观锁像"信任制"——我先改,提交时检查有没有被别人改过。没冲突直接成功,有冲突就重试。SQL 是UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock >= 1,一句话说完。

悲观锁像"门禁制"——我先锁门,改完了再开门。别人要改同一件商品?排队等着。SQL 是SELECT ... FOR UPDATE,先锁住这一行,其他事务读取同一行会被阻塞。

怎么选?看场景:

  • 日常购物:同时买同一件商品的概率不高,乐观锁够用,性能好。
  • 秒杀活动:几千人同时抢 10 件商品,冲突概率极高,必须悲观锁。

商城的策略:普通商品用乐观锁,秒杀商品用悲观锁。既保证性能,又保证安全。

订单查询与接口设计

商城规范:所有订单操作统一传数字主键 id。

classOrderViewSet(viewsets.ModelViewSet):"""订单接口"""permission_classes=[IsAuthenticated]defget_queryset(self):"""只返回当前用户的订单"""returnOrder.objects.filter(user=self.request.user)# GET /api/orders/ → 订单列表# GET /api/orders/{id}/ → 订单详情(传数字主键 id)# POST /api/orders/ → 创建订单(下单)# POST /api/orders/{id}/cancel/ → 取消订单# POST /api/orders/{id}/complete/ → 确认收货# POST /api/orders/{id}/refund/ → 申请退款

为什么用id而不是order_no?

  • order_no是给用户看的,用于客服沟通、物流查询。
  • 数字主键id是数据库层面的唯一标识,查询效率最高(主键自带索引)。
  • 统一用id,前后端对接更简洁。如果有的接口传id、有的传order_no,前后端同学不得打起来?

取消订单 & 退款

取消和退款不只是改个状态——还得把库存还回去。核心思路:

@action(detail=True,methods=['post'],url_path='cancel')defcancel(self,request,pk=None):"""取消订单 + 回滚库存"""order=self.get_object()# 1. 状态机校验(只有 pending → cancelled 是合法的)transition_order(order,'cancelled')# 2. 回滚库存(遍历订单项,把扣的库存加回去,用 F('stock') + quantity)# 这一步和改状态必须在同一个事务里!returnResponse({'code':0,'message':'订单已取消'})

退款逻辑类似,paid → refunded或shipped → refunded,库存同样要回滚。

注意:取消和退款的库存回滚也应该放在事务里。如果改完状态、回滚库存到一半崩了,就会出现"订单取消了但库存没还"的惨剧。


用 AI 生成状态机代码

状态机这种东西,特别适合让 AI 来写。因为模式非常固定:

  1. 定义状态常量
  2. 定义转换规则表
  3. 写校验函数
  4. 写执行函数

这四步,AI 几秒钟就能搞定,而且质量很高。

prompt 是这样的:

你是一个 Django 开发者。请为电商订单系统实现一个状态机。 状态列表:pending、paid、shipped、completed、cancelled、refunded 要求: 1. 用字典定义状态转换规则 2. 实现 can_transition() 校验函数 3. 实现 transition_order() 执行转换,同时记录时间节点 4. 所有非法转换抛出 ValidationError 5. 代码风格符合 PEP 8,带中文注释

AI 给出的代码和我们的实现几乎一致,甚至还会额外加日志记录和状态标签映射。

不过有一点要注意:AI 生成的事务代码,你一定要亲自检查。因为 AI 不知道你项目的具体业务逻辑——比如"优惠券过期了要不要回滚"、“部分退款怎么算”,这些都需要人来把关。

AI 加速心得:状态机、校验逻辑、序列化器这类"规则明确、模式固定"的代码,放心交给 AI。但涉及业务判断、事务边界的代码,AI 只是助手,你才是决策者。


学完来练练手

  1. 加一个"超时自动取消":订单创建后 30 分钟未支付,自动变成已取消、库存回滚。(提示:Celery Beat 定时任务,或 Django Signals)

  2. 加一个"部分退款":订单里 3 件商品,用户只退其中 1 件。库存怎么回滚?金额怎么算?状态怎么变?(提示:可能需要新增一个partial_refunded状态)

  3. 加一个"订单状态变更通知":每次状态变更时给用户发推送通知。(提示:Django Signal 的post_save,检测status字段变化)

  4. 给乐观锁加重试机制:当条件更新返回affected = 0时,不直接报错,等 50ms 后重试,最多 3 次。(提示:time.sleep(0.05)+for循环)

做完你会发现,一个真实的订单系统远不止今天这些内容。但核心框架——状态机 + 事务 + 锁——已经搭好了,后面都是在这个基础上长肉。


配套文件day10_order.py(状态机 + 下单事务 + 乐观锁/悲观锁双版本 + DRF 接口)可参考学习。

本系列为梅雅达编程笔记原创,首发于 CSDN。有问题?评论区见

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

Eclipse中创建Maven工程:从零搭建到依赖管理实战

1. 为什么在IDE里建Maven工程这件事值得单独拿出来讲很多人第一次接触Java项目构建&#xff0c;都是从"新建一个工程"这个动作开始的。看起来只是点几下鼠标的事&#xff0c;但真正动手之后你会发现&#xff0c;同样叫"创建Maven工程"&#xff0c;在不同工…

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

微信桌面版聊天数据本地解析方案:C语言DB块级解密与结构化导出

简介&#xff1a;这是一套基于C语言开发的微信聊天记录备份与分析工具&#xff0c;面向计算机专业学生、毕业设计开发者及对本地数据深度分析有需求的技术人员&#xff0c;解决64位微信环境下Key获取受限、多账号管理低效、聊天内容难以结构化导出与检索等实际问题。资源包共68…

作者头像 李华
网站建设 2026/10/18 15:44:51

MoE架构深度解析:从稀疏激活到负载均衡的关键技术

聊到模型效率&#xff0c;混合专家模型&#xff08;MoE&#xff09;架构几乎是绕不开的话题。它与传统的密集模型不同&#xff0c;MoE把网络拆成若干专家子网络&#xff0c;让每一条输入只经由其中一小部分专家处理。这不仅把计算量压下来&#xff0c;还能在同样的算力预算下把…

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

Windbg深度解析:Windows内核调试与蓝屏诊断核心指南

简介&#xff1a;本资源是面向Windows系统开发者、驱动工程师及高级运维人员的Windbg调试工具实战资料包&#xff0c;聚焦内核与用户态程序的深度分析、蓝屏崩溃排查及性能瓶颈定位等核心场景。压缩包共307个文件&#xff0c;涵盖82个DLL&#xff08;调试扩展与运行时依赖&…

作者头像 李华
网站建设 2026/10/18 15:44:23

Python Flask构建天气可视化分析系统:从数据采集到ECharts图表展示全流程

天气可视化分析系统&#xff0c;在开发者的练手项目清单里属于永远不会过时的那一类。它的覆盖面很有意思&#xff1a;数据采集、清洗入库、SQL聚合、接口封装、前端图表渲染&#xff0c;一条完整链路全都能碰到。我这次基于 Python Flask 做了一套带源码、数据库和文档的版本…

作者头像 李华