首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >同城外卖怎么上云:高峰削峰、订单-运单解耦与腾讯云拼装

同城外卖怎么上云:高峰削峰、订单-运单解耦与腾讯云拼装

原创
作者头像
码兄科技
发布于 2026-11-03 17:54:59
发布于 2026-11-03 17:54:59
1410
举报

问题从哪来:同城外卖把双峰 QPS 与履约弹性拧在一起

县城外卖怕的是稀疏运力派不出去,校园外卖怕的是整点脉冲与楼栋履约;同城外卖的矛盾更像「商圈密度高、午晚双峰陡、一单可跨多店、骑手可改派、配送费会溢价」。用户在 CBD 或住宅商圈 11:40 同时点两家店,支付成功后商家要接单出餐,履约侧要在 GEO 候选池里指派、超时改派,资金侧还要扛住支付回调重放与部分退款——任何一层用「一张订单表硬改状态」顶住,高峰都会在同一分钟把故障放大。

失败形态高度集中——下单卡、派单飘、改派乱、退款双花、观测盲——背后往往是同一类工程缺口:订单与运单耦合、购物车跨店没有行项目快照、调度只有指派没有改派契约、动态配送费事后改价无审计、支付回调与通知同事务、CLS 只堆日志没有业务指标。本文按云上工程边界拆:领域分层与腾讯云组件选型、订单/运单状态机、GEO 指派与改派、表结构草图、REST/事件契约、双峰时序、幂等支付退款、失败模式与观测、测试矩阵。不讨论首页怎么好看,也不讨论商务交付形态;只讨论系统怎么在同城双峰里削得住、派得准、退得清。

领域分层:先画边界,再点名组件

同城外卖建议按域切开,而不是按「小程序页面」堆接口。一层逻辑对应一类扩展与一类故障面:

分层

职责

腾讯云侧常见落点(举例)

接入层

鉴权、限流、路由、WAF、签名

API 网关

交易域

多店购物车、下单、支付回调、退款

云托管 / 云函数 + 云数据库 MySQL

商品域

商家、菜单、营业态、售罄、配送范围

MySQL + Redis

计价域

基础配送费、距离阶梯、高峰溢价、满减

云托管计价服务 + Redis 规则缓存

履约域

运单、GEO 候选、指派/改派、轨迹

云托管 + TDMQ/CMQ + Redis GEO

异步与通知

支付后处理、订阅消息、超时/改派扫描

TDMQ/CMQ + 云函数消费者

对象与日志

商品图、售后凭证、链路与业务日志

COS + CLS

配置与多城

城市开关、商圈、溢价时段、运力池

配置中心 / 环境变量 / CloudBase 环境隔离

小程序或 H5 前端可用 CloudBase 静态托管与云开发能力做轻量落地;核心交易、计价与调度建议放到云托管(容器),便于按午晚双峰水平扩展。同城日单量与 QPS 通常明显高于县城/校园,默认单地域起步,但要按商圈分片与热点表归档,不要为了「看起来像大厂」一上来拆成十余个微服务。硬约束四条:

  1. 订单与运单解耦——商家接单不等于运力已锁定;一单多店可生成多运单或合并取餐策略,但状态机必须分开。
  2. 下单主路径同步、通知与积分异步——不要把订阅消息写进支付回调的同一同步链。
  3. 计价结果落快照——高峰溢价、距离费、满减以快照为准,禁止事后静默改价。
  4. 改派是一等公民——超时、拒单、异常必须有原因码与可回放事件,而不是后台手工改 rider_id。

订单与运单状态机:先画转移,再写接口

同城外卖的核心对象是「订单实例 + 运单实例(可一对多)」,不是「商家工作台截图」。订单状态建议至少覆盖:

状态

含义

允许的下一跳

CREATED

已创建待支付

→ PAID / CANCELLED(超时关单)

PAID

支付成功,待商家确认

→ ACCEPTED / PARTIAL_ACCEPTED / REFUNDING

ACCEPTED

全部商家已接单,制作中

→ READY / REFUNDING

PARTIAL_ACCEPTED

多店场景部分商家已接

→ ACCEPTED / REFUNDING(未接部分)

READY

出餐齐套或可分批发运

→ FULFILLING

FULFILLING

履约中(由运单驱动)

→ COMPLETED / EXCEPTION

COMPLETED

全部送达完成

终态(售后走独立单据)

EXCEPTION

履约异常

→ FULFILLING / REFUNDING / COMPLETED(人工结案)

REFUNDING / REFUNDED

退款中 / 完成

按资金结果收敛

AFTER_SALE

售后处理中

→ REFUNDED / COMPLETED

运单(waybill)建议独立状态机,与订单用事件对齐,禁止骑手端直接 UPDATE orders SET status:

运单状态

含义

允许的下一跳

PENDING_DISPATCH

待调度

→ ASSIGNED

ASSIGNED

已指派待接

→ ACCEPTED_BY_RIDER / REASSIGNING(拒单/超时)

REASSIGNING

改派中

→ ASSIGNED / EXCEPTION

ACCEPTED_BY_RIDER

骑手已接

→ PICKED / REASSIGNING(异常上报后退回)

PICKED

已取餐

→ DELIVERING

DELIVERING

配送中

→ DELIVERED / EXCEPTION

DELIVERED

已送达

终态

EXCEPTION

异常进人工台

→ REASSIGNING / CANCELLED

CANCELLED

运单取消

终态(订单侧另走退款或重建运单)

多店购物车:订单行按 merchant_id 分组;可「一单一运单合并取」或「一店一运单」。两条硬约束:

  1. 转移必须带原因码与操作者(用户取消、商家拒单、支付超时、调度超时、骑手拒单、用户联系不上、溢价规则版本)。裸改状态字段无法复盘「单去哪了」。
  2. 取餐、送达、改派、退款动作必须幂等。弱网下骑手重复点「已取餐」「已送达」、调度重复触发改派非常常见;同一 waybill_id + action + idempotency_key 重放应返回同一业务回执。

PARTIAL_ACCEPTED 与 REASSIGNING 是同城相对县城/校园更不能省的中间态:少了前者,多店场景只能「全接或全退」;少了后者,高峰拒单只能靠人工改库,事故复盘永远缺证据。

调度:Redis GEO 指派 + 改派,而不是全市最短路炫技

同城商圈密度高,调度要先做 geo shard(商圈/网格),再在 shard 内做候选排序;不要每次 GEORADIUS 扫全市。推荐模型:

  1. 商圈字典 zone:zone_id、多边形或网格、高峰时段、基础 ETA 区间、溢价系数版本。
  2. 下单锁定 zone_id + 计价快照:后续调度、限流、运力池、报表都按商圈维度。
  3. Redis GEO:骑手心跳 GEOADD rider:geo:{zone_id};负载与拒单率另存 Hash;指派时 GEORADIUS 取候选后按「距离 + 当前负载 + 完单率 + 新手保护」打分。
  4. 指派 → 超时/拒单 → 改派:ASSIGNED 超时进入 REASSIGNING,写入改派原因码,重新选人并发 waybill.dispatch.reassigned;连续 N 次失败进 EXCEPTION 人工台。不要无限扩半径把空驶打穿。
  5. 改派锁:SET reassign:{waybill_id} NX EX,防止超时扫描与人工台双改派。

动态配送费:计价服务根据距离阶梯、天气/高峰规则版本计算,结果写入订单 fee_snapshot_json;骑手端展示的「本单预计收入」也来自快照,避免调度改派时费用漂移引发纠纷。

表结构草图(可单体,边界按域切开)

早期可单体部署,表与接口按域切开,后续拆服务少搬家。

订单 city_order

  • id, city_id, zone_id, user_id
  • cart_mode(SINGLE / MULTI_MERCHANT)
  • status, pay_amount, delivery_fee, surcharge_fee, discount_amount
  • fee_snapshot_json, price_rule_version
  • address_id, address_snapshot
  • eta_from, eta_to
  • idempotency_key, version

订单行 city_order_item

  • order_id, merchant_id, sku_id, qty, price_snapshot
  • merchant_accept_status(多店接单进度)

运单 waybill

  • id, order_id, zone_id, merchant_id(可空表示合并取)
  • rider_id(可空), status
  • dispatch_attempt, assign_score_snapshot
  • assigned_at, accepted_at, picked_at, delivered_at
  • reassign_reason_code, exception_code
  • version

改派日志 waybill_reassign_log

  • waybill_id, from_rider_id, to_rider_id, reason_code, operator, at

骑手位置(热):Redis GEO + 最近心跳;冷轨迹可异步落库或 COS 对象,避免写爆 MySQL。

退款 refund_ticket

  • id, order_id, refund_request_id(唯一), amount, scope(整单/行项目), status

审计 order_audit_log

  • order_id / waybill_id, from_status, to_status, reason_code, operator, at

索引至少覆盖:(zone_id, status, created_at)、(merchant_id, status, created_at)、(rider_id, status)、(user_id, created_at)、(status, assigned_at)(超时改派扫描)、refund_request_id 唯一。

REST 与事件契约(草图)

用户端 / 商家端 / 骑手端打到同一后端,用角色鉴权拆开:

代码语言:bash
复制
POST /api/v1/carts/quote                     # 多店询价(含动态配送费)
POST /api/v1/orders                          # 创建订单,Header: Idempotency-Key
POST /api/v1/orders/{id}/pay-callback        # 支付回调(验签后投递队列)
POST /api/v1/orders/{id}/merchants/{mid}/accept
POST /api/v1/orders/{id}/ready               # 齐套或分批发运触发运单
POST /api/v1/waybills/{id}/assign            # 系统/人工指派
POST /api/v1/waybills/{id}/reassign          # 改派(幂等)
POST /api/v1/waybills/{id}/accept            # 骑手接单
POST /api/v1/waybills/{id}/pickup            # 取餐,幂等
POST /api/v1/waybills/{id}/deliver           # 送达,幂等
POST /api/v1/waybills/{id}/exception
POST /api/v1/refunds                         # 退款(幂等键)
GET  /api/v1/riders/me/waybills:pull

领域事件(TDMQ/CMQ Topic 示例名):

  • order.paid / order.merchant_accepted / order.ready
  • waybill.created / waybill.dispatch.assigned / waybill.dispatch.reassigned
  • waybill.dispatch.timeout / waybill.picked / waybill.delivered / waybill.exception
  • fee.quoted(可选审计) / refund.succeeded

支付回调只做验签、落「已支付意图」、投递 order.paid;真正的商家推送、运单创建、订阅消息由消费者处理。双峰回调堆积时靠队列削峰,保证最终一致,而不是把网关线程拖死。

高峰时序:双峰削峰、批量拉单、改派扫描

假设场景:工作日 11:30–12:45 与 17:30–19:15,核心商圈 QPS 脉冲,多店购物车占比升高,骑手拒单率随空驶上升。推荐主路径时序:

  1. 接入层:API 网关对 POST /orders 与 POST /carts/quote 做城市级令牌桶 + 用户级配额;超限返回业务码 RATE_LIMITED。菜单与商圈配置走 CDN/Redis。
  2. 询价与下单:计价服务读规则版本 → 写 fee_snapshot → 创建 CREATED。幂等键:user_id + cart_hash + 时间窗。
  3. 支付回调:验签 → 标记支付意图 → 发 order.paid → 快速 200。
  4. 商家侧:多店按行项目接单;支持部分接单;高峰用 orders:pull 批量拉,而不是一单一推打爆通道。
  5. 出餐与运单:齐套或允许分批时创建运单;调度服务按 zone GEO 指派。
  6. 改派扫描:云函数定时或队列延迟消息扫描 ASSIGNED 超时单,进入 REASSIGNING。
  7. 骑手侧:心跳写 GEO;pickup/deliver 幂等;异常码标准化。
  8. 售后:未取餐取消、配送超时赔付走 refund_ticket,不在骑手 App 里直接改资金字段。

云上拼装示例(按需取舍):接入用 API 网关;交易/计价/调度跑云托管;尖峰消费者、超时改派、对账用云函数订 TDMQ;菜单/售罄/GEO/限流计数进 Redis;订单运单进云数据库 MySQL;图片与凭证进 COS;网关错误率、下单成功率、队列堆积、改派率、退款失败率进 CLS。CloudBase 适合管理端或原型;调度与计价复杂度上来后,核心路径放到可水平扩的云托管更可控。

幂等支付与退款:双花和对账事故都出在这里

  1. 下单幂等:Idempotency-Key 唯一索引;冲突返回原单。
  2. 支付回调幂等:按支付平台 transaction_id 去重;已处理直接 ACK。
  3. 退款幂等:refund_request_id 唯一;多店部分退按行项目与运单维度回滚配送费分摊,规则写进快照,避免「退菜不退溢价」扯皮无据。
  4. 改派幂等:同一 waybill_id + attempt_no 只产生一次有效指派结果。
  5. 送达/取餐幂等:已是目标状态返回成功回执,而不是假冲突。

失败模式要提前进手册:支付成功但消费者失败 → 队列重试 + 对账;改派成功但旧骑手仍显示在途 → 以服务端状态为准强制拉单;GEO 点漂移 → 以 zone 归属为主、距离为辅;溢价规则热更新 → 只影响新询价,不影响已落快照订单。

失败模式、观测与容量

故障

表现

工程应对

双峰下单打满

网关超时、询价变慢

令牌桶 + 询价缓存短 TTL + 可读限流

支付回调堆积

已付款未见单

快 ACK + TDMQ 削峰 + 对账

骑手拒单潮

出餐积压

超时改派 + 限制扩半径 + 人工台

改派双写

一单两骑手

改派锁 + 落库唯一在途约束

多店部分失败

用户只收到一半

PARTIAL_ACCEPTED + 行级退款

配送费纠纷

客服无法解释

fee_snapshot + 规则版本

重复送达

状态错乱

动作级幂等

弱网商家端

接单丢失

本地补拉 + 补打小票接口

观测(CLS)分级建议:

  • P1:下单成功率、支付回调失败率、退款失败率、队列死信
  • P2:接单延迟、指派超时率、改派率、异常单占比、询价 P99
  • P3:慢 SQL、GEO 心跳缺失、COS 上传失败、云托管副本 CPU

容量:云托管按午晚定时扩缩;Redis 按 zone 分 key;MySQL 热点订单与历史归档分离。安全侧:退款/改价/改派审计;演练伪造 zone_id、越权接单、刷改派。

上线前最低测试矩阵

  1. 同一 Idempotency-Key 重复下单不双建单
  2. 支付回调重复投递状态一致
  3. 多店部分接单与行级退款金额正确
  4. 询价快照与支付金额一致,规则热更新不影响旧单
  5. 指派超时自动改派,并发改派只有一人成功
  6. 骑手重复取餐/送达幂等
  7. 异常上报进入人工台并可再次指派
  8. 回调堆积时下单与接单仍可用
  9. 伪造商圈参数、越权改派被拒绝
  10. 配送费分摊与部分退款对账一致

常见工程坑:把订单状态直接当运单用,导致改派与多店无法建模;动态配送费不落快照;只有指派没有改派事件;把订阅消息和积分写进支付同步链;UTC 与业务时区混用导致高峰窗统计错乱。把这些写进运维手册,成本远低于第一个双峰客服救火。

同城相对县城的关键增量,不只是「骑手更多」,而是计价、改派、多店三条链路同时变热:任何一条用临时字段或后台手工改库顶住,双峰时的客服工单都会呈指数上涨。把规则版本、改派日志与行级退款从第一天写进模型,比事后补表便宜一个数量级。

小结

同城外卖在工程上要优先保证五件事:订单/运单解耦且可审计、多店购物车行级状态清晰、GEO 分片下的指派+改派可回放、动态配送费快照化、支付与通知用消息队列削峰并让退款幂等。API 网关、云托管、云函数、TDMQ/CMQ、Redis GEO、云数据库 MySQL、COS、CLS,以及可选 CloudBase,是按边界拼装的零件,用「例如」对齐能力即可。UI 与运营位是触达手段;削不住峰、派不准人、退不清钱的系统,菜单再漂亮也只是把故障放大。先把一两个核心商圈的状态机、改派与双峰限流跑稳,再复制到下一商圈,才是可持续的扩展方式。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 问题从哪来:同城外卖把双峰 QPS 与履约弹性拧在一起
  • 领域分层:先画边界,再点名组件
  • 订单与运单状态机:先画转移,再写接口
  • 调度:Redis GEO 指派 + 改派,而不是全市最短路炫技
  • 表结构草图(可单体,边界按域切开)
  • REST 与事件契约(草图)
  • 高峰时序:双峰削峰、批量拉单、改派扫描
  • 幂等支付与退款:双花和对账事故都出在这里
  • 失败模式、观测与容量
  • 上线前最低测试矩阵
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档