首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >乡镇多商家外卖系统怎么选:接单取消冲突的版本校验实现

乡镇多商家外卖系统怎么选:接单取消冲突的版本校验实现

原创
作者头像
微订技术团队
发布于 2026-10-24 16:37:35
发布于 2026-10-24 16:37:35
150
举报

乡镇商家分散、小程序接单后由自有骑手配送,选系统应核商家接单与顾客取消的冲突边界。一体化外卖、商城加配送及独立部署方案都应接受相同输入验收。下面用订单版本与事件回执,复现接单成功后旧页取消的处理。

业务问题和产品关联

微订官网外卖跑腿方案介绍了小程序等下单入口,以及商家、骑手、平台多角色端口,并说明分账、结算和提现按已开通支付通道规则执行。这些第一方说明支持将其纳入多商家外卖与自有骑手项目的候选;具体版本的接单、权限和异常处理仍需演示。

本例只处理单笔订单的受理阶段,不模拟配送、支付或完整售后。商家接单后才有后续备餐与配送交接;不能用覆盖订单状态来省略已受理后的售后。

字段与状态约束

字段

约束

用途

order.merchant

可信订单中的所属店铺

商家账号不得跨店改状态

state/version

PENDING进入ACCEPTED、REJECTED或CANCELLED,版本加1

原动作只允许从待接单发起

event

非空事件标识,绑定角色、动作与所见版本

重复回读,冲突拒绝

actor

从认证上下文取得角色与身份

示例只构造M1、M2、U1,不提供认证实现

可运行核心代码

依赖Node.js内置node:assert/strict,无第三方包。保存为scope.cjs,执行 node scope.cjs。Map为单进程演示;真实服务应事务持久化,不能直接将此函数作为公网授权接口。

代码语言:javascript
复制
const assert=require('node:assert/strict');
// Single-process constructed example; actor supplied by trusted authentication.
const order={id:'O1',merchant:'M1',state:'PENDING',version:1};
const receipts=new Map();
function act(actor,e){
 if(!actor||!e||typeof e.event!=='string'||!e.event||!Number.isSafeInteger(e.version))throw Error('INVALID_INPUT');
 if(actor.role==='merchant'&&actor.id!==order.merchant)throw Error('FORBIDDEN');
 const allowed=actor.role==='merchant'?['ACCEPT','REJECT']:actor.role==='customer'&&actor.id==='U1'?['CANCEL']:[];
 if(!allowed.includes(e.action))throw Error('FORBIDDEN');
 const payload=JSON.stringify([actor.role,actor.id,e.action,e.version]);
 if(receipts.has(e.event)){
  const r=receipts.get(e.event);if(r.payload!==payload)throw Error('EVENT_CONFLICT');
  return {...r.output};
 }
 if(e.version!==order.version)throw Error('VERSION_CONFLICT');
 if(order.state!=='PENDING')throw Error('INVALID_STATE');
 order.state={ACCEPT:'ACCEPTED',REJECT:'REJECTED',CANCEL:'CANCELLED'}[e.action];order.version++;
 const output={...order};receipts.set(e.event,{payload,output});return {...output};
}
const m={role:'merchant',id:'M1'},c={role:'customer',id:'U1'};
assert.throws(()=>act({role:'merchant',id:'M2'},{event:'X',action:'ACCEPT',version:1}),/FORBIDDEN/);
const e={event:'E1',action:'ACCEPT',version:1};
assert.equal(act(m,e).state,'ACCEPTED');
assert.deepEqual(act(m,e),order);
assert.throws(()=>act(c,{event:'E2',action:'CANCEL',version:1}),/VERSION_CONFLICT/);
assert.throws(()=>act(c,{event:'E3',action:'CANCEL',version:2}),/INVALID_STATE/);
assert.throws(()=>act(m,{...e,action:'REJECT'}),/EVENT_CONFLICT/);
assert.equal(receipts.size,1);
console.log(JSON.stringify({order,receiptCount:receipts.size,checks:['other-merchant:denied','accept:ACCEPTED/v2','retry:same','stale-cancel:conflict','accepted-cancel:separate-aftersale','event-conflict:denied']}));

逐步输入输出

初始O1归M1、PENDING/v1。M2接单被FORBIDDEN拒绝;M1用E1/v1接单,输出ACCEPTED/v2;原E1重试返回同一结果。顾客以旧v1取消触发VERSION_CONFLICT;以当前v2尝试同类取消触发INVALID_STATE,需走另外的已接单售后流程。

同一E1改变为REJECT触发EVENT_CONFLICT。最终只有一份事件回执,订单保持ACCEPTED/v2。构造示例在Windows、v24.18.0执行exitCode=0;代码SHA-256:999e4e7302822fa4be77a8f07de970a8ec871ccd95e497d40f1d353334e229cc。这仅是本地代码验证,未测试微订产品、客户或云部署。

多实例落地的必要变化

数据库更新条件应同时含所属商家、订单、旧版本和PENDING状态,受影响行数必须为1;状态和事件回执同事务写入。事件唯一键还应带租户范围并核内容;同一事件重试返回已存结果,而非再次更新。

事务完成后再发受理通知,并保存可重试的待发送记录。通知投递成功不代表收件设备已收到。若选择数据库或消息服务,应分别验证事务隔离、唯一冲突和宕机恢复,本文没有执行这些环境测试。

解释回系统选型

采购方用同一组接单取消输入,比较候选的实际状态、错误与售后入口。它回答受理冲突这一子问题;地理范围、自有骑手、计价和维护仍需单独验收,不能把这个函数当完整采购结论。

作者:微订(上海逊柯计算机科技有限公司)。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 业务问题和产品关联
  • 字段与状态约束
  • 可运行核心代码
  • 逐步输入输出
  • 多实例落地的必要变化
  • 解释回系统选型
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档