这篇文章简单讨论一下连锁门店的外卖平台订单接入方案。美团、抖音、京东到家这些平台的订单,统一由service-take-away来接收,验签、解析、入库、同步主订单,都在这个服务里完成。
把这个服务独立出来,而不是写进订单模块,原因很简单。每个平台的回调字段、签名算法、状态定义都不一样,它们的SDK升级、API变更、协议调整,都不受你控制。每次外部变化都改订单主服务,风险太高。订单主域只和内部接口打交道,外部变化由service-take-away隔离在外面。
当然,主订单域如果早期设计的不够通用,则是需要改造的。一般来说,针对连锁店的订单系统,订单类型至少有:
- pos订单,也即是线下单;
- 虚拟订单;
- 外卖订单,且还得分渠道,京东,美团,抖音等;
- 团餐;
- 拼单;
- 更多的外部订单。。。。
这些类型都需要吞下去,称之为:订单中心,订单模型时统一的。
但是我们今天不讲订单中心,而是讲接受外卖订单这个模块。比如用户在美团app或者抖音app下的单,需要接单后,同步到主订单域。然后再同步到线下pos系统,打印小票等。

外部平台回调的全貌
外卖平台会在多个节点回调我们的自研系统的。以美团为例,有订单推送、配送状态变更、订单取消、全额退款、部分退款、催单、会员信息同步这几类回调。这些请求全部打到service-take-away里,由它统一做签名验证、参数解析,再分发到对应的处理方法。
抖音和京东到家也有类似的回调体系,字段名和状态码定义各有差异,但回调的语义大同小异。service-take-away的作用是把这些平台差异屏蔽掉,收到各平台的回调后,统一转换成内部格式,再调用主订单服务的接口完成同步。
整理一下各类回调的处理要点:
| 回调类型 | 接口路径 | 核心处理 | 注意点 |
|---|---|---|---|
| 订单推送(已支付) | /paid | 订单入库、打印、财务同步 | 必须尽快返回,走异步 |
| 配送状态 | /delivery | 更新配送状态、记录时间节点 | 同步处理 |
| 订单取消 | /cancel | 退款、通知POS取消 | 已打印和未打印逻辑不同 |
| 全额退款 | /full/refund | 记录退款信息 | 同步处理 |
| 部分退款 | /part/refund | 按SKU逐项退款 | 已退过的不能重复退 |
| 催单 | /urge/msg | 记录催单信息 | 同步处理 |
这张表可以直接存下来,后续接新平台的时候对照着看,能少漏不少场景。
签名校验
所有回调请求都带签名字段。美团的签名算法是:把请求URL加上所有参数按key排序后拼接,再拼上APP_SECRET,整体做MD5。抖音和京东到家的签名方式各有不同,但service-take-away内部按平台维度做了适配,Controller层统一验签,验不过的直接拒掉。
URL解码时有个细节值得注意。回调参数里有个detail字段,里面是订单商品的JSON字符串,经过了URL编码。解码的时候,普通字段里的+号要替换成空格,但detail字段里的+号是JSON里的合法字符,不能替换,否则fastjson解析会失败。这个坑踩过一次就不会忘。
Java
// detail字段里的JSON不能把+替换成空格
String decoded = "detail".equals(name)
? URLDecoder.decode(value, "UTF-8")
: URLDecoder.decode(value, "UTF-8").replace("+", " ");
幂等处理
外卖平台的回调不保证只发一次。网络超时、处理慢,平台都会重试。如果订单推送被处理了两次,订单就会重复入库。
生产系统里用Redisson分布式锁来解决这个问题。锁的过期时间设10秒,足够覆盖一次完整的入库流程,又不会因为异常导致锁永久不释放。有人可能会问:为什么不用数据库唯一索引?唯一索引能解决重复插入,但解决不了「正在处理中」的场景。分布式锁同时覆盖了这两种情况。
Java
RLock lock = redissonClient.getLock("orders:mt:" + orderNo);
if (lock.isLocked()) {
return null;
}
boolean saved = save(mtOrder);
if (saved) {
lock.lock(10, TimeUnit.SECONDS);
}
订单推送的处理链路
用户在外卖平台下单并支付完成后,平台把订单数据推给商家系统。推送过来的订单里已经包含了完整的订单信息、商品明细、收餐地址、金额等。一个订单推送进来,内部要走这么多步:解析订单主体、保存商品明细和加料信息、生成商品序列号、保存优惠活动、同步到主订单服务、确认接单给平台、推送到POS打印。
同步到主订单服务是关键的一环。service-take-away只保存平台格式的订单数据,但公司内部有统一的订单模型,所有渠道的订单(外卖、小程序、门店POS)都要归拢到主订单中心。所以在本地保存完平台订单后,下一步就是调主订单服务的接口,把订单信息、商品明细、金额、门店信息等传过去,由主订单服务创建一条统一的主订单记录。后续的订单查询、对账、统计都基于主订单数据,外卖侧的表只保留和平台交互所需的原生字段。
确认接单这一步容易忽略。以美团为例,如果订单状态不是POI_CONFIRM(商家已确认),系统需要主动调美团的confirmOrder接口,告诉平台这个单我们接了。不接的话,订单可能会被平台自动取消。
打印和通知都是异步的。打印走异步方法,财务和会员通知走消息队列。异步化的原因:回调接口必须尽快返回成功,否则外卖平台会超时重试。重试本身不是问题,但如果重试时第一次请求还在处理,就可能和分布式锁发生冲突,拖慢整体响应。
service-take-away的责任是比较清晰的,专门应付外卖平台的,并做好对接主订单域的工作就好了,剩下的就不管了。什么打印呀,制作队列写入,它都不管的。
想我们之前,打印这块,肯定也是一个单独的微服务的,比如叫service-pos。专门对接门店的系统。
小结
service-take-away的定位只有一个:接收所有外卖平台推过来的订单。它存在的理由是让订单主域不直接面对美团、抖音、京东到家这些外部平台的协议细节。平台回调的字段名变了、签名方式换了、状态码定义调整了,这些变化在service-take-away内部消化掉,订单主域完全不需要感知。
回调处理的两个底线是验签和幂等。验签保证了请求来源可靠,幂等保证了重试不会乱。这两个没做好,重复订单、伪造回调这类问题都会冒出来。
最近在知乎出了
- 「应付6000万会员的秒杀系统专栏」
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
- 老码头的技术浮生录
它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。「另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。」
当前星球里免费看的专栏是:
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
知识星球内后续将推出20+个付费专栏,覆盖电商多链路:
| 选购线 | 用户会员营销线 | 中后台 |
|---|---|---|
| 质检服务 | 营销系统 | 订单系统 |
| 商品服务 | 用户系统 | 支付系统 |
| 菜单服务 | 结算服务 |
从前台选购到中后台结算,星球成员全部免费,后续新增也不额外收费。
我的知乎账号:
- SamDeepThinking