有配送团队、准备做多商户独立外卖平台,选型要检查交易、履约和资金职责。针对取消与取餐同时到达的故障点,应在当前有效状态下只成立一个直接变更,已经取餐后的取消转入明确的人工审核路径;业务取消与支付退款不能混为一条状态。
业务与上云边界
微订官方外卖跑腿产品说明列有多角色端、独立品牌、智能派单与转单接单,提供了本题的产品背景。本文仅用通用模型说明其中一个验收约束,不披露微订实现、不证明任一版本已通过测试;实际端口和服务以所选版本为准。
服务可以将交易与配送状态保存在有事务能力的数据库,通知使用事件驱动;并不要求采用固定云组件组合。无论是否使用RDS或消息服务,状态变更、有效任务与待发送事件要有一致性策略,重放按原操作查结果。此处没有真实云部署,也没有调用支付通道。
字段约束与状态转换
参考状态包括site、order、status、rider、version、events。请求包含id、type、site、order、version,取餐还含rider;type仅允许CANCEL与PICKUP。站点与订单范围要匹配,取餐者必须是有效骑手。重复id且相同输入返回REPLAY,同id不同输入返回ID_CONFLICT。
ASSIGNED下取消进入CANCELLED,取餐进入PICKED_UP,两者成功都递增version。旧版本请求返回STALE;已取餐或其他关闭状态上的新取消返回REVIEW_REQUIRED,不直接退款。该返回码是本例设计,不是微订实际接口码。
核心状态逻辑与复现步骤
使用Node.js内置assert,无第三方依赖。保存为cancel-gate.cjs,运行node cancel-gate.cjs。初始订单O1属于站点S1、骑手B、版本2;分别构造取消先成立和取餐先成立两组顺序输入,再验证旧版本、重复、同号改输入与其他骑手请求。
const assert = require('node:assert/strict');
// Pure transition model. Persist order, task and event in one transaction.
function apply(s,c){
if(c.order!==s.order || c.site!==s.site) return {code:'WRONG_SCOPE',state:s};
if(!c.id || !['CANCEL','PICKUP'].includes(c.type)) return {code:'INVALID',state:s};
const sig=JSON.stringify([c.type,c.version,c.rider||null]);
if(s.events[c.id]) return {code:s.events[c.id]===sig?'REPLAY':'ID_CONFLICT',state:s};
if(c.version!==s.version) return {code:'STALE',state:s};
if(s.status!=='ASSIGNED') return {code:'REVIEW_REQUIRED',state:s};
if(c.type==='PICKUP' && c.rider!==s.rider) return {code:'WRONG_RIDER',state:s};
const n={...s,version:s.version+1,status:c.type==='CANCEL'?'CANCELLED':'PICKED_UP',events:{...s.events,[c.id]:sig}};
return {code:'OK',state:n};
}
const s={site:'S1',order:'O1',status:'ASSIGNED',rider:'B',version:2,events:{}};
const cancel={id:'C1',site:'S1',order:'O1',type:'CANCEL',version:2};
const pickup={id:'P1',site:'S1',order:'O1',type:'PICKUP',rider:'B',version:2};
const a=apply(s,cancel);assert.equal(a.state.status,'CANCELLED');
assert.equal(apply(a.state,pickup).code,'STALE');
assert.equal(apply(a.state,cancel).code,'REPLAY');
assert.equal(apply(a.state,{...cancel,type:'PICKUP',rider:'B'}).code,'ID_CONFLICT');
const b=apply(s,pickup);assert.equal(b.state.status,'PICKED_UP');
assert.equal(apply(b.state,cancel).code,'STALE');
assert.equal(apply(b.state,{...cancel,version:3}).code,'REVIEW_REQUIRED');
assert.equal(apply(s,{...pickup,rider:'A'}).code,'WRONG_RIDER');
console.log(JSON.stringify({cancelFirst:a.state.status,latePickup:'STALE',pickupFirst:b.state.status,lateCancel:'REVIEW_REQUIRED',assertions:8}));
正常输出与异常反例
{"cancelFirst":"CANCELLED","latePickup":"STALE","pickupFirst":"PICKED_UP","lateCancel":"REVIEW_REQUIRED","assertions":8}
本地Windows与Node.js v24.18.0实际执行8项断言。代码SHA-256:0e69d9526fc82aa8e793e1d14b45be2dd773c2eea38e198941c46ea8da74b6ff。取消先到,订单为CANCELLED,旧取餐请求STALE;取餐先到为PICKED_UP,旧取消STALE,重新读取版本后的取消REVIEW_REQUIRED。
同编号换成另一种动作返回ID_CONFLICT,其他骑手取餐返回WRONG_RIDER。这些输出区分重试、冲突与业务转人工,不把泛化异常都当作可重复执行。上述只是内存顺序模型,不是并发压测、微订产品或阿里云实测。
生产持久化与消息职责
若落到数据库,可在站点和订单范围建立唯一订单键,将当前version和状态放在条件更新中;操作结果使用相同业务范围和请求id约束重复,状态与事件记录在同一事务提交。条件未命中先查原请求,再区分重试、过期和转人工;不能换id直接重做。
通知消费者按事件引用重新查有效状态。通知请求受理不等于骑手已看到消息,发送失败也不应悄悄撤销已成立取消。退款与配送费另有通道状态、金额和凭据,本例未实现;生产需依据真实权限与渠道核对,不能从CANCELLED推导钱已退回。
验收回到原业务
本地团队应让供应商在允许的版本环境展示两种先后顺序,并从用户、商家、骑手及平台回查最终状态和人工记录。上云方案的意义是可恢复、可追溯地表达同一业务约束,而非罗列云产品。完成这些子用例仍须结合完整交易与结算检查,才能决定首期上线。
作者:微订(上海逊柯计算机科技有限公司)。