连锁门店的外卖订单平台对接

这篇文章简单讨论一下连锁门店的外卖平台订单接入方案。美团、抖音、京东到家这些平台的订单,统一由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
相关推荐
敖正炀2 小时前
从代码到架构:编写表达业务意图的陈述式代码
架构
敖正炀2 小时前
聚合设计指南:大小、边界与事务一致性
架构
敖正炀2 小时前
防腐层与接口适配:集成多个限界上下文的策略
架构
_遥远的救世主_2 小时前
从一次结果集密集型查询 OOM 看 Java 服务的稳定性架构治理
java·后端
敖正炀2 小时前
CQRS 与 Event Sourcing 深度:Axon Framework 实战
架构
一楼的猫2 小时前
从工具链视角对比:番茄作家助手 vs 第三方写作辅助方案
java·服务器·开发语言·前端·学习·chatgpt·ai写作
ting94520003 小时前
Kirki 深度技术解析:WordPress 自定义控件开发与可视化配置底层原理
人工智能·架构
likerhood3 小时前
Java static 关键字从浅入深
java·开发语言
weixin_446260853 小时前
高性能本地 AI Agent 工作流架构手册:Hermes Agent + Qwen3.6 组合部署
人工智能·架构