县城外卖系统选SaaS还是私有化,不能只确认数据能导出,还要确认切换时谁允许写入、失败如何回退。下面针对旧端未冻结或新端已有订单却直接回退的问题,给出带版本的切换状态与核心实现,用正常输出和异常反例支撑采购验收。
场景与业务边界
微订第一方产品资料列有标准SaaS产品,外卖跑腿方案公开说明支持独立私有部署,并提供用户、商家、骑手和平台等角色端。县城采购方可以据此分别核对两种交付方案;具体版本、模块、数据导出范围、迁移服务和维护责任,需要落实到演示与合同。
构造旧端old和目标端new。只模拟写入权限与状态,不连接真实订单、支付或消息。旧端冻结后,校验未通过不能切换;目标端已出现新写入时,必须先对账而非直接回退。所有转换应由受控服务执行,本例不实现身份认证。
状态与字段约束
| 字段 | 约束 | 转换 |
|---|---|---|
| state | SOURCE/FROZEN/TARGET | SOURCE冻结后才可校验切换 |
| epoch | 转换时递增 | 旧版本请求拒绝 |
| checked | 仅冻结阶段校验通过设置 | 未校验不得开放新端 |
| writes | 示例累计写入计数 | 有写入时保守拒绝直接回退 |
完整核心逻辑与复现
使用Node.js内置断言,无第三方依赖。保存为change.cjs,运行node change.cjs。该单进程内存模型演示约束,真实系统需事务、持久化状态和对所有写入入口统一校验。
const assert = require('node:assert/strict');
class Cutover {
constructor(){
this.state='SOURCE';this.epoch=1;this.active='old';this.checked=false;this.writes=0;}
freeze(epoch){
assert.equal(epoch,this.epoch,'STALE');assert.equal(this.state,'SOURCE','STATE');this.state='FROZEN';this.epoch++;return this.epoch;}
verify(epoch,ok){
assert.equal(epoch,this.epoch,'STALE');assert.equal(this.state,'FROZEN','STATE');assert.equal(ok,true,'DIFF');this.checked=true;}
switch(epoch){
assert.equal(epoch,this.epoch,'STALE');assert.equal(this.state,'FROZEN','STATE');assert.ok(this.checked,'UNCHECKED');this.state='TARGET';this.active='new';this.epoch++;}
write(side,epoch){
assert.equal(epoch,this.epoch,'STALE');assert.equal(side,this.active,'INACTIVE');assert.ok(['SOURCE','TARGET'].includes(this.state),'FROZEN');this.writes++;}
rollback(epoch){
assert.equal(epoch,this.epoch,'STALE');assert.equal(this.state,'TARGET','STATE');assert.equal(this.writes,0,'RECONCILE_REQUIRED');this.active='old';this.state='SOURCE';this.checked=false;this.epoch++;}
}
const c=new Cutover();const frozen=c.freeze(1);
assert.throws(()=>c.write('old',frozen),/FROZEN/);
assert.throws(()=>c.switch(frozen),/UNCHECKED/);
assert.throws(()=>c.verify(frozen,false),/DIFF/);
c.verify(frozen,true);c.switch(frozen);
assert.throws(()=>c.write('old',c.epoch),/INACTIVE/);
assert.throws(()=>c.write('new',frozen),/STALE/);
c.write('new',c.epoch);
assert.throws(()=>c.rollback(c.epoch),/RECONCILE_REQUIRED/);
const clean=new Cutover();const e=clean.freeze(1);clean.verify(e,true);clean.switch(e);clean.rollback(clean.epoch);
assert.equal(clean.active,'old');
console.log(JSON.stringify({
active:c.active,state:c.state,epoch:c.epoch,writes:c.writes,cleanRollback:clean.active,unsafeRollback:'rejected'}));
实际输入输出与异常
SOURCE、epoch1冻结后变FROZEN、epoch2,冻结期写入和未校验切换均拒绝。校验false拒绝;校验true后切换为TARGET、epoch3。旧端写入、旧epoch请求拒绝,新端以epoch3写入一次成功。
再请求回退得到RECONCILE_REQUIRED。另一全新实例未产生写入,完成校验切换后允许回旧端。实跑输出active=new、state=TARGET、epoch=3、writes=1、cleanRollback=old、unsafeRollback=rejected。Windows v24.18.0退出码0,代码SHA-256:c96fcae36d0e5d5c5a1845d0823766ed51fc3b01e643e913c2a46906891cccd4。
上云实现需要增加什么
数据库以项目号保存状态和epoch,转换使用条件更新;受影响行数不为1时回读冲突,不能继续开放写入。业务写入事务应检查同一份可授权状态,不能只在页面或网关缓存一个开关。回调、消息消费和后台任务也应纳入约束。
模型累计计数采取保守规则,源端写过也会阻止直接回退;生产应区分切换后的增量并逐笔对账。verify的布尔值不是实际数据校验器,需要由可信的差异校验结果驱动,且绑定批次、截止点和版本,不能让客户端自行声明通过。
本例没有多进程互斥、断电恢复、外部接口事务或自动补偿。实际系统需演练崩溃后恢复、在途回调、失败重试及人工接管;应用服务、数据库和存储按真实职责配置,不假定某云组件天然解决业务切换。
把技术反例用于采购
要求服务方在拟用版本解释冻结入口、增量边界、回退条件及责任,保存真实输出。两类部署方案都应做业务验收;本例是本地构造逻辑,不是微订产品或云部署测试,也不能证明迁移无损或所有方案具备此实现。
作者:微订(上海逊柯计算机科技有限公司)。