一、订单表怎么设计:一张主表加流水
订单主表记一笔外卖订单的当前状态,流水表记它经历过的每一次状态变更。只留主表不留流水,后面排查“这单到底卡在哪一步”时就两眼一抹黑。
order_main: id, channel(meituan/eleme), channel_order_no,
store_id, amount, status, created_at, updated_at
order_event: id, order_id, from_status, to_status,
payload(JSON), occurred_at
channel_order_no 是平台那边的订单号,和自家 order_id 双轨对应。这一列要建唯一索引——同一个平台订单号无论被推送多少次,在主表里只能落一条记录,这是后面幂等的地基。
二、幂等处理:重复推送是常态不是例外
平台推送新单、推送状态变更,都可能因为网络超时而重试。你的回调接口如果不做幂等,同一笔单就会被建两次。
做法很直接:收到推送先按 channel_order_no 查主表,已存在就跳过创建、只补事件流水;不存在才插入主表。整个“查—插”放在一个事务里,靠那个唯一索引兜底。
on_order_push(payload):
no = payload.channel_order_no
if exists(order_main, no):
write_event(no, payload); return 'duplicate_ok'
insert_order_main(payload) -- 唯一索引冲突即说明已被并发插入
状态回写同理:把“待接单→已接单”这条转换做成条件更新,只有当库里当前状态确实是待接单时才翻过去,重复的旧状态推送不会把已经出餐的单又改回待接单。
三、消息队列解耦:别在回调里干重活
平台的回调接口要求你几毫秒内返回成功,不然它会一直重推。所以回调里只做一件事:验签、落一条原始消息到队列,立刻返回成功。真正的解析、建单、打小票、通知,全部异步消费。
这样做的好处是把“对平台负责”和“对业务负责”拆成两段。平台只等你确认收到,慢一点的业务处理不影响它的重试判断。消费失败的消息进重试队列,超过次数进死信队列人工介入,不会因为一次抖动就丢单。
四、小票图片:别把二进制塞进数据库
后厨要打的小票、用户的支付凭证截图、餐品打包照片,这些都是文件,不进 RDS。业务表只存一个文件标识,真正的文件放对象存储:
order_receipt: order_id, oss_key, uploaded_at
上传成功后把 oss_key 写回订单,库里不存图片字节
小票机出单时按 oss_key 去取图片,打完即焚。订单量一大,把图片字节塞数据库会让 RDS 的备份和查询都变慢,这是早期最容易犯的错。
五、通知触达:状态变更给商家和顾客发条短信
新单进来、骑手到店、订单完成这几个关键节点,要让商家和顾客及时知道。通知别自己养通道,统一走短信服务:业务系统只在状态翻转时发一条消息,由短信服务按报备好的模板发出,不占业务服务的资源。通知和主流程解耦——短信发失败了不能反过来让订单回滚。
六、对账机制:每天打烊后跑一遍
白天订单实时流转,晚上要做一次对账:自家系统里今天每个渠道的订单数、金额,和平台侧导出来的流水,逐笔勾稽。对不上的单子挂异常表,第二天人工核查。
这种按日跑的批处理很适合放在函数计算上:定个每天打烊后的触发器,扫当天已完结订单聚合出每个渠道的总数和总额,和平台流水比对,生成对账差异报告。平时不占常驻机器,到点拉起、算完释放。
daily_reconcile():
for ch in [meituan, eleme]:
ours = agg(order_main, ch, today)
theirs = import_channel_file(ch, today)
diff = compare(ours, theirs)
if diff: write_exception_report(diff)
七、适用规模与技术局限
主表加事件流水、回调幂等、队列异步、文件对象化、定时对账,这一套足够支撑单店到几十家连锁门店的外卖对接,工程重心在一致性和可对账,而不是复杂的营销玩法。
边界也要说清楚。日订单到十万级以上,order_event 这种流水表要按时间分片,别和主表挤在一起;多渠道多门店时订单主表要挂门店标识做隔离;对账涉及金额,差异处理要留全量操作痕迹,谁、什么时候、把哪笔单调成了什么样,都能追。规模没到之前别过早分库,先把幂等和对账做扎实,这两件事才是这类系统的命门。
几个常被问到的点
问:为什么回调里不能直接建单?答:回调要秒级返回,建单、打小票、通知链路长,一旦卡住平台会疯狂重推。落队列异步化,回调只管确认收到。
问:重复推送为什么不会建两次单?答:平台订单号唯一索引加事务,查到已存在就只补事件流水,重复请求天然幂等。
问:小票为什么不放数据库?答:图片字节会拖慢 RDS 备份与查询,对象存储更便宜且自带冗余,库里只存文件标识。