校园外卖经过校门中转时,校外骑手把餐送到站点,收餐人员确认并分拣,校内配送人员再送到楼栋。同一笔订单会连续经过几个岗位,页面上的一个“配送中”很难说明餐在哪里、由谁保管。
如果把“骑手到站”“站点收餐”“校内人员接手”都归入同一个完成按钮,出了漏单就很难还原交接过程。本文以这个场景为例,讨论状态划分、重复扫码、并发领取和批量操作的设计。文中的字段与伪代码用于说明方案,不代表某个已上线系统,也不包含实际压测结论。
先约定三种动作的含义:
到达记录不能代替接收记录,线上领取成功也不等于餐品已经离开站点。系统应根据不同事实更新状态,而不是根据按钮名称推断责任。
在只讨论校门接收至楼栋交付这一段的情况下,可以采用下面的主流程:
待站点接收 -> 已接收 -> 已分拣 -> 已领取 -> 校内配送中 -> 已交付状态变化 | 需要确认的事实 | 建议保留的记录 |
|---|---|---|
待接收 → 已接收 | 站点人员核对并收到餐品 | 订单标识、接收人、站点、服务端时间 |
已接收 → 已分拣 | 餐品已经放入明确的楼栋分组 | 分组标识、操作人、分拣位置 |
已分拣 → 已领取 | 一名有权限的配送人员取得任务 | 任务编号、执行人、任务版本 |
已领取 → 校内配送中 | 餐品从站点交给该配送人员 | 交出岗位、接收人、交接时间 |
校内配送中 → 已交付 | 餐品送到约定的交付位置 | 交付方式、位置、确认人及凭据 |
自取场景还可以单独记录“用户已取走”。是否以送到取餐点作为配送完成,需要在业务规则中明确;发送取餐通知不能直接产生“用户已取走”记录。
取消、破损和餐品不符应有单独的异常处理入口。需要更正误操作时,记录原因和更正事件,保留原始交接历史。
订单适合保存商家、收货地址快照和交付方式;分段配送任务保存谁执行、从哪里到哪里、进行到哪个阶段。交接记录则说明餐品如何从一个岗位转到另一个岗位。
数据对象 | 示例字段 | 设计目的 |
|---|---|---|
订单 |
| 保留用户确认的交付条件 |
配送任务 |
| 追踪每段配送及状态版本 |
交接记录 |
| 保存上下游任务的关联与接收事实 |
操作请求 |
| 识别重试并查询原结果 |
一个订单存在多段任务时,不宜靠不断覆盖订单上的“当前骑手”来保存完整过程。可以维护当前执行人的查询字段,但历史责任仍需从任务与交接记录还原。
地址也需要保留快照。楼栋改名、站点迁移以后,历史订单应能显示当时确认的地址。在途任务如果要改目的地,应通过有记录的调整操作完成。
下面的事务示例假定相关数据位于同一关系数据库。若任务与交接由不同服务分别写入,需要另外设计消息去重、失败恢复和一致性核对,不能把本地事务的保证直接套用过去。
扫码后没收到响应,客户端无法据此判断操作失败:请求可能没有到达服务端,也可能已经提交,只是响应丢失。
客户端应在第一次提交前生成并保存 request_id,重试继续使用原值。服务端为带租户、操作者作用域的请求键建立唯一约束,并保存请求内容摘要及处理结果。
同一个键再次出现时,先重新核对调用者身份和权限,再比较内容摘要。内容一致则返回原处理结果;内容不同应报告请求键冲突,不能覆盖原记录。
但这还不够。用户再次扫码可能生成一个新键,服务端仍须检查业务状态。例如,一份餐已经被站点接收,新请求不能再创建一次接收记录。必要时为同一次交接建立业务唯一约束;退回后重新交接则使用新的交接编号。
请求去重回答“这次调用是否已处理”,状态检查回答“这项动作现在是否还能执行”。两者都需要。
两个人同时打开待配送列表,都可能看到同一笔任务。前端检查“尚未领取”只能改善提示,不能阻止并发提交。
下面是单数据库下的一种事务处理思路,省略了具体语言和表结构:
校验登录身份、校区与站点权限
开始事务
按作用域和 request_id 获取或创建请求记录
对同键请求加锁串行处理;记录已存在时校验内容摘要
若已有处理结果,结束事务并返回保存的结果
锁定目标任务
在事务内核对任务归属、状态、版本与操作者资格
若任务不是“已分拣”或已有执行人:
保存本次冲突结果,不修改任务
否则:
将执行人设为当前操作者,状态改为“已领取”
任务版本加一,写入领取事件,保存成功结果
提交事务
返回处理结果并发创建同键请求记录时,唯一约束冲突需要进入读取原记录的路径;锁等待超时或事务回滚应按原请求键恢复。发生死锁时,还应使用有上限的重试策略。
任务锁让同一任务的领取按顺序判断。第一人提交后,第二人看到的已经是更新后的状态,因此只能得到冲突结果。另一种实现是条件更新,以任务编号、原状态和原版本作为条件,根据影响行数判断是否成功。
无论采用哪一种,任务变化、领取事件和请求结果都应在同一事务提交。否则可能出现任务已领取、原请求却查不到结果的情况。
这一操作只分配执行资格。餐品离开站点时,还要按交接规则确认实际接收。
一组扫码任务可能包含正常订单、重复订单和不属于本站点的订单。对于彼此独立的任务,可以逐项提交事务,再汇总结果,避免一项错误使其余任务全部失败。
每项都应有稳定的请求键。批次编号用于关联本次操作,不代替单项幂等键。
{
"batchId": "batch-example",
"items": [
{"requestId": "req-a", "taskId": "task-a", "result": "accepted"},
{"requestId": "req-b", "taskId": "task-b", "result": "already_applied"},
{"requestId": "req-c", "taskId": "task-c", "result": "state_conflict"}
]
}这是返回格式示意。界面可以分别显示“本次成功”“已处理”和“状态冲突”,不要把所有非成功项都包装成一个“重试”按钮。
断网导致结果不明时,客户端先按请求键查询。查不到结果仍不能断言原请求没有执行,可以保留原键重试,由服务端去重。已经成功的项目无需重新生成请求。
如果业务要求整箱餐品必须整体交接,则应先定义箱与订单的绑定和整体事务边界,不能直接采用逐单独立提交的方案。
离线期间产生的“到站”事件,可能晚于“已接收”事件到达服务端。按最后收到的一条消息覆盖状态,会把任务退回较早阶段。
提交事件时可以附带预期任务版本。服务端结合允许的状态转换判断:已处理的事件返回原结果;过期但可留作补充的观察保存到事件记录;与当前状态冲突的操作转入核对流程。设备时间可以作为辅助信息,不能单独决定状态先后。
通知发送也应与交付状态分开。例如,在保存交付记录的同一事务中写入通知待发送表,提交后再由后台任务发送。这种事务消息表常被称为 outbox。
后台发送失败可以重试。若外部通知服务已发送、程序却在记录结果前中断,恢复后可能再次发送,因此还需使用事件标识去重,并结合通知服务的幂等能力处理。仅增加一张待发送表,不能保证外部通知绝不重复。
通知失败不撤销已经确认的交付;通知成功也不补造用户取餐记录。
联调时,除正常流程外,还应检查下面几项。它们是建议验证的条件,并非本文已经完成的测试结果。
场景 | 应验证的结果 |
|---|---|
同键同内容连续提交 | 返回原结果,业务动作只生效一次 |
同键换了任务或操作内容 | 明确冲突,原请求记录不被覆盖 |
不同请求同时领取同一任务 | 只有一名执行人成功获得任务 |
数据库已提交但客户端超时 | 使用原键可以查询或恢复结果 |
批次内混入状态冲突项 | 每项结果可查,成功项不重复执行 |
过期事件晚到 | 当前状态不被直接回退,冲突可追踪 |
其他校区账号操作任务 | 服务端拒绝越权访问与修改 |
交付后通知发送失败 | 交付记录保留,通知独立恢复 |
排查一笔异常订单时,系统应能沿订单找到分段任务,再找到接收人、交接时间和原操作结果。明确这些关系以后,用户端、商家端、配送端和管理端才能对同一笔订单给出一致的解释。
作者:微订(上海逊柯计算机科技有限公司)。本文讨论校园配送场景的通用系统设计。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。