从业务逻辑出发来拆解外卖系统
做产品或者做技术的人,迟早会遇到一个绕不开的系统——外卖系统。我第一次完整接触外卖系统的业务逻辑时,第一反应是“这不就是电商加个配送吗”,真深入下去才发现,外卖系统比纯粹的电商交易要复杂得多。它有三个强实时性角色的协同、有严格的时间窗口、有动态的供需匹配,还要应对各种“人”带来的不确定性:用户会等不及取消、商家会漏看单、骑手会爆胎。任何一个环节出问题,状态的流转就全乱了。
这篇文章我想换个角度,不直接上来画架构图、列技术栈,而是先从业务逻辑出发,把外卖系统从一单完整的交易开始,拆到每一环的决策逻辑、状态变更和异常处理。适合正在做外卖类产品的新手PM、刚接手外卖系统的后端开发,以及想搞懂“系统里的人是怎么协作的”的创业者。搞懂了业务逻辑,技术选型和架构设计只是水到渠成的事。
1. 一单外卖背后到底发生了几件事
1.1 外卖不是“点餐+配送”这么简单
很多第一次接触外卖系统的人,会下意识地把外卖理解成“用户下单,商家出餐,骑手送过去”,这个理解没错,但它停留在物理世界。数字化系统要做的事情,比这多得多。物理世界里的每一步,在系统里都需要被拆成时间点、事件、状态、责任人、补偿机制。
举一个最简单的例子。用户点了一份盖浇饭,“下单”这件事在系统里不是一个动作,而是一连串的动作:用户提交订单,系统要校验地址是否在配送范围内、店铺是否营业、餐品是否在售、库存是否够、用户账户是否正常、优惠券是否可用;通过校验之后,要锁定库存、创建支付单、拉起支付;支付成功回调之后,才真正生成一条“待商家接单”的订单,推送给商家端。
同样,一单配送也不是“骑手从A到B”。它包含了骑手接单、到店、报备、取餐、确认取餐、开始配送、到达、确认送达,每一个动作背后都对应一个GPS位置采集点、一个状态变更、一个给用户的推送通知。这些动作组合起来,才是完整的订单生命周期。
所以拆解外卖系统的正确姿势,不是先看有几个微服务、用了什么消息队列,而是先把“一单外卖从产生到完成,整个过程在真实世界里会发生哪些事”完整列出来。业务逻辑不清,架构再先进也没用。
1.2 核心角色与交易链路的三个关键闭环
外卖系统里至少有四个角色在同一个交易链路里协同:用户、商家、骑手、平台。每个角色都有自己的核心诉求,系统存在的意义就是平衡这四方的利益。
用户的诉求是“快、准、稳”——餐出得快、送得快、别送错、别撒漏。商家的诉求是“接单效率高、出餐压力小、数据清晰”——最好订单自动接,出餐时间预估准。骑手的诉求是“派单合理、路线顺畅、时间宽松”——别让我空跑、别让我爬六楼还得超时。平台的诉求更复杂:用户体验要好、商家的供给要丰富、骑手的运力要稳定,同时平台自己的成本和收益也要可控。
这三个角色之间的交易链路,核心是三个闭环:
- 交易闭环:用户下单 → 支付 → 商家接单 → 出餐 → 骑手配送 → 用户确认收货 → 结算给商家和骑手。
- 履约闭环:商家出餐 → 骑手取餐 → 配送中 → 送达 → 异常处理和售后。
- 信息闭环:用户看到的餐品信息、商家看到的订单信息、骑手看到的取送信息,必须是同一份数据源,任何一端的数据不一致,立刻引发投诉。
把这几个闭环在脑子里过一遍,你就会发现:外卖系统的核心不是“技术有多牛”,而是“状态流转是否可靠、异常处理是否兜得住”。业务逻辑拆解的第一步,就是把这三个闭环里的每一个节点讲清楚。
2. 订单生命周期:贯穿全局的那条主线
2.1 从“购物车”到“已支付”:用户侧的状态流转
订单是整个外卖系统的核心实体,订单状态就是系统的“主动脉”。我习惯把订单从创建到完成的状态流转画成一条线,再标出每个状态节点上的触发事件和责任人。
用户侧的状态通常这么走:待支付 → 已支付 → 商家已接单 → 餐品制作中 → 骑手配送中 → 已送达。如果中间出问题,还会有已取消、退款中、退款完成、异常单等状态。
这个状态序列看似简单,但每条转移路径背后都有对应的业务规则。举个最常见的场景:用户提交订单进入待支付状态,此时系统已经锁定了库存,但还没有真正占用商家的产能。超过15分钟未支付,系统自动关单,库存释放。这个时间窗口不能随便定,太短了影响用户支付体验,太长了商家侧的订单积压概率增加。大多数平台把这个值设在这个区间,还要针对不同场景做动态调整,比如高峰期短一些,深夜用户可能付款慢。
再看看支付。支付成功回调到达系统后,订单从“待支付”切到“已支付”,同时要触发一系列动作:通知商家端有新订单待处理、给用户发下单成功的推送、开始计时商家接单超时。这里有一个非常关键的工程问题:支付回调是异步的,如果回调丢失或者延迟,订单状态就会卡死。所以系统必须有主动查单的对账机制,不能只依赖被动回调。
2.2 商家接单与出餐:平台如何做状态同步
订单推到商家端之后,商家的接单策略会直接影响订单状态流转。现在主流的外卖平台都有两种接单模式:手动接单和自动接单。手动接单适合小店,老板看到单子确认一下,防备忙不过来;自动接单适合连锁和出餐稳定的商家,系统接单后直接进入制作流程。
商家端的状态流转通常是:待接单 → 已接单 → 制作中 → 已出餐。这里最容易被忽略的业务逻辑是“出餐”到底以什么为准。如果商家点了“出餐”,但骑手还没到店,那餐品只能放在台面上等,保温、口感都会受影响;如果骑手到了店,商家还没出餐,骑手就得在店里干等。为了解决这个衔接问题,系统里引入了两个时间节点:预计出餐时间和预计到店时间。系统根据餐品种类、门店历史出餐效率、骑手距离和路况,动态计算两边的匹配度,尽量让骑手到店时间与出餐时间重合。
这里要特别讲一下出餐上报机制,很多新手设计系统时会忽略它。商家出餐后点击“出餐”,这个动作不仅仅是改状态,它同时会触发:骑手端收到取餐提醒、系统重新计算骑手等餐时长、如果骑手还未到店,系统会评估是否要改派其他骑手。出餐上报的准确性直接决定了骑手端的调度效率和用户体验,所以有些平台会做商家出餐超时的处罚/奖励机制,本质就是为了让商家如实上报状态。
2.3 骑手配送与妥投:履约链路的最后一环
骑手端的订单状态比商家端更细:待接单 → 已接单(赶往商家) → 已到店 → 取餐中 → 已取餐(前往用户) → 已送达。
这一连串状态里,有两个非常考验业务逻辑的点。第一是到店/取餐的报备,它涉及骑手与商家的权责切分。骑手点了“到店”之后如果餐没做好,等餐时间算谁的?如果骑手没点“到店”就直接取餐走了,餐品少了算谁的?所以系统必须用状态时间戳把这些账算清楚,这也是后面结算、赔付的重要依据。
第二个点是送达确认。现在主流平台默认“点击送达即视为完成”,但在业务逻辑上,这里会有恶意提前点送达的风控规则:骑手距离配送地址过远时点送达,系统会拦截或者标记异常。反过来,用户侧收到的状态是“已完成”,但实际没收到餐,又会有“用户未收到餐”的售后流程。所以“送达”这个状态,在系统里并不是一个单纯的订单状态,它关系到骑手收入结算、平台服务评分、用户的售后入口。
到这一步,一单正常外卖在主流程上的状态流转就全走完了。理解了这条主线,你再看外卖系统的其他模块,都是围绕这条主线的“辅助系统”。
3. 业务规则与状态机设计:谈清楚“为什么这么设计”
3.1 状态机是外卖系统的“骨架”
主流程的状态梳理清楚了,下一步要落地的就是状态机,没有状态机的订单系统就是一团浆糊。状态机的核心价值有两个:一是让订单的流转路径确定性明确——每一步都在设定的合法范围内;二是让状态变更之后的动作触发有迹可循。
设计状态机的时候要注意几个原则。第一,状态不能乱跳。比如“已支付”的订单不能直接变成“已完成”,中间漏了“商家接单”和“骑手配送”就不行。第二,状态变更要有事件来源。每次状态变更必须对应一个触发事件,可能是用户操作、商家操作、骑手操作、定时任务或支付回调。第三,同一个事件在不同状态下要允许做不同的处理,而不是只依赖状态判断。
我举一个特别常见的例子:取消订单。取消这个动作,在订单的每个阶段都有不同的语义和规则。待支付状态下,用户取消是无条件的,系统直接关单;已支付但商家未接单,用户取消要进入“退款”流程;商家已接单但骑手未取餐,用户取消可能会触发“赔付商家”的费用判断;骑手已取餐再取消,规则就更严格了,可能要走客服介入。如果不把取消设计成一个带着业务规则的状态机,而只是在代码里硬写 if-else,后续每加一条规则都会成为新的故障点。
再讲一个容易被忽略的细节:状态机必须有超时机制。每个状态节点都可能有“卡住”的情况——用户不支付、商家不接单、骑手不取餐、用户不确认收货。这些节点都要设计定时器去触发兜底动作:自动关单、超时提醒、自动改派、自动确认收货。没有兜底的外卖系统,会堆积大量“僵尸订单”。
3.2 取消、退款、异常单:边界情况才是真正考验
如果说主流程是高速公路,那取消、退款和异常就是泥泞小路,但恰恰是这些小路最能考验业务逻辑设计得是否扎实。
先说退款。外卖的退款和电商有一个很大的区别:外卖有明确的时效性,餐品是即时制作的,而且部分餐品一经制作就无法二次销售。所以外卖系统退款必须做“实扣实退”的精细化设计:用户支付的金额、平台给的优惠补贴、商家承担的折扣,每一部分都要算清楚谁承担损失。最常见的场景是用户取消订单,系统要给用户退全额,但商家已经备料了,这部分成本谁来背?不同平台有不同的规则,本质上是平台和商家之间的博弈,而系统要做的就是准确记录每个环节的时间戳和状态,为后续判责提供依据。
再说异常单。这个大类里包括:商家出餐超时、骑手超时未取餐、用户拒收、地址无法送达、餐品撒漏、用户长时间不接电话等等。异常单的最优解不是“发生后处理”,而是“发生时快速分流”。系统要根据异常类型自动触发不同的处理流程:是继续等待、改派骑手、还是取消订单并全额退款。优秀的业务逻辑,在异常发生时就已经能自动判断“该走哪条路”,而不是把所有情况都推给人工客服。
我给一个建议:在设计业务逻辑图的时候,把“正常流程”和“异常/补偿流程”分开画。正常流程用于开发主流程的骨架,补偿逻辑则单独梳理成规则矩阵,两者结合才能支撑起完整的系统。
4. 系统的核心模块拆解与数据流
4.1 四端联动的协作逻辑
外卖系统的业务逻辑决定了它的技术模块边界。按照角色来分,系统至少可以拆成:用户端、商家端、骑手端、平台管理端,加上底层的交易中心、履约中心、结算中心、消息中心。每个端背后都知道“同一个订单”的不同截面。
用户端看到的是“我的订单”,它关心订单状态是啥、骑手在哪、餐什么时候到。商家端看到的是“待处理订单、制作中、已完成”,它关心的是什么时候该出餐、什么时候骑手到店。骑手端看到的是“被派给自己的订单”,它关心的是取餐点、送餐点、路线、时间预算。平台管理端看到的则是全部订单的实时状态、异常监控和规则配置。
四端的数据必须围绕订单ID做聚合。用户在App上取消订单,商家端和骑手端几乎同步就要看到订单状态变化,不能出现用户取消了、商家还在出餐的情况。这背后靠的是可靠的消息推送和状态同步机制,但从业务逻辑上讲,它靠的是“统一状态源、状态变更广播”的设计原则。谁修改了订单状态,这个事件必须发给所有关心这个订单的端。
这里我有一个很深的体会:业务逻辑拆解时,最容易漏掉的是“端与端之间的预期管理”。比如骑手取餐后,用户端的 Status 还是“商家制作中”,用户就会焦虑。所以正常的设计会在关键节点给用户推送状态变化通知,甚至提供骑手实时位置轨迹。承载这些体验的背后,是每一个状态节点上“要不要通知”“通知谁”“用什么文案”的业务规则。
4.2 从创建到归档的数据走向
从数据的视角来看,外卖订单经历了这样的生命周期:创建(写入订单表) → 变更(状态机驱动,修改状态) → 履诺(履约过程中产生新数据:骑手位置、操作记录) → 结算(金额分摊、核算) → 归档(进入离线数仓,用于数据分析和复盘)。
在创建阶段,订单数据会涉及多个子域的写入:订单主表、订单明细表、支付单、配送单、营销使用的优惠明细。这里有一个工程上很重要的问题:订单创建和库存扣减必须保持原子性。用户提交订单后,扣减库存成功但订单创建失败了,库存就丢了;订单创建成功但扣减库存失败,就会出现超卖。现在比较稳妥的做法是通过本地消息表加事务消息来保证最终一致性,但业务逻辑设计上,要明确“什么算下单成功”——通常以订单表创建成功为准,其他动作异步补偿。
在履约阶段,更多的数据是流水和轨迹。骑手的GPS位置、每分每秒都在写入位置表,这些数据的读压力非常大,所以通常会做汇聚和削峰。订单状态变更的历史也要记录下来,这就是我们常说的状态变更流水表。这张表太重要了,运营要复盘、客服要判责、财务要核账,全都看它。我自己做业务逻辑设计时,有一个很土但很有效的做法:确保任何一条订单状态,都能根据流水表和时间字段完整还原出“这个订单在什么时间点经历了什么”。
订单完成后进入结算和归档阶段。结算关注的不是订单本身状态,而是每笔订单的钱怎么分:平台服务费、商家收入、骑手配送费、用户退款和补偿。这些规则可以由产品灵活配置,但底层的数据源都是订单上的时间戳、金额字段和状态变更记录。
5. 实操中那些让人头大的问题与排查思路
5.1 状态不一致:最常见也最隐蔽的问题
我接手过的外卖系统排查案例里,一半以上都跟“状态不一致”有关。典型的表现是:用户端显示“支付成功”,商家端没有收到新订单;骑手端已经点了“送达”,用户端还停留在“配送中”;用户已经取消订单,系统照样给用户推送了“骑手已取餐”。这些问题归根到底,都是几个端的数据没有对齐。
排查这类问题,我的思路分四步。
第一步,先定位“源状态”和“目标状态”。确定哪个端的状态是准确的、哪个端的同步延迟了。如果可能,直接查订单主表的状态,以它为准。
第二步,查状态变更流水。看这条订单最后几次状态变更分别是什么时候、由谁触发的。如果流水显示“已支付”被写入,但商家端没收到,问题大概率出在消息推送环节,而不是订单状态本身。
第三步,查消息队列的积压情况。很多状态不同步是消息积压导致的,骑手端报了位置,消息推到队列里半天没被消费,用户端的轨迹就卡住了。
第四步,查并发场景。两个请求同时修改同一个订单状态——比如用户取消的同时,骑手点了送达。这时候如果没有做好状态机校验,后到达的请求可能直接把状态覆盖掉,导致“已取消的订单被送达了”这种脏数据。
解决状态不一致的核心思路不是“出了错再修”,而是在写入时做合法转移校验、在异步链路里做消息幂等、在关键节点做定时对账补偿。这三层都做好了,状态不一致的概率会大幅下降。
5.2 超时、并发、库存:三个绕不开的坎
外卖系统是典型的高并发实时系统,尤其在午晚高峰,流量会瞬间冲上来。我总结下最常遇到的三个坎。
第一个坎是超时。前面讲过很多定时任务和状态超时:未支付关单、商家超时未接单提醒、骑手超时未取餐改派。这些定时任务在高峰期同一时刻可能触发几百万条,如果全部扫描数据库,DB 压力会非常可怕。实际项目中,会把任务按时间分桶、用延迟队列触发,同时也别一把扫全表,尽量用游标分批拉取。
第二个坎是并发下的库存。外卖和电商不一样的地方在于,它不仅是“总量库存”,还涉及分时产能。比如一个爆款单品的当日销量上限是100份,商家实际接单能力可能到中午就饱和了。系统的库存模型要考虑“商品维度”和“店铺维度”两层,不能只扣菜品库存,还要看这家店此刻还有没有产能接新单。扣减时要防止超卖,常用的手段是数据库乐观锁update ... set stock = stock - 1 where stock > 0,或者用 Redis 预扣加异步对账。
第三个坎是骑手调度并发。再准确的时间预估,也顶不住实际的早高峰路况。骑手同时收到多个新订单推送、多个订单同时被申请改派,如果调度系统没有做好“同一骑手同一时间只能有一个主任务”这种去重约束,就会出现两个订单都派给同一个骑手的冲突。业务逻辑上要设计好骑手的状态机:空闲、任务中、忙碌,只有空闲状态才能被派单。
这些坎不是靠某一个中间件就能解决的,更多要靠业务逻辑层面的降级和约束。我在设计系统时始终提醒自己:别把期望压在“最优解”上,高峰期能保证“不崩、不错、能兜底”,就已经是合格的外卖系统了。
6. 从“能跑”到“好用”的经验沉淀
从业务逻辑出发拆解外卖系统,最大的收获不是画出多少张流程图,而是理解了系统里每个角色背后的诉求和约束。用户要的是确定感,商家要的是效率,骑手要的是合理的路和时间,平台要的是全局平衡。业务逻辑设计的本质,就是在这几个目标之间找那个动态平衡点。
最后分享一个我自己踩过的坑:早期设计订单状态时,只考虑了正常路径,漏了“商家接单后但骑手长期未到店”这类边界状态,结果上线后每天的异常单处理都靠手工后台。后来在订单状态机上增加了“等待超时”的定时流转,并按分钟级巡检,才算把这个问题兜住。做外卖系统,别追求把所有场景一次性想全,但一定要把兜底机制设计好。系统可以允许偶发的业务异常,但不能没有应对异常的规则和流程。
如果你正在做外卖系统的设计或开发,我建议的第一步不是选框架、建仓库,而是拿着纸笔,把自己当成一个用户在App里完整点一单,把每一步看到的、感受到的都记下来;再把自己当成商家接一单、当成骑手送一单。三个视角走完,业务逻辑的骨架也就出来了。剩下的事情,无非是把这套逻辑用代码忠实地实现出来。