乡镇外卖SaaS选型时,未来能否迁移需要核数据和未结业务接续。本文使用Python标准库SQLite实现迁移批次从prepared到verified再到switched的约束:校验租户、修订和事件幂等,并阻止仍有未结订单或退款的批次切换。
业务范围与方案职责
微订官方产品与业务全景说明可选择标准SaaS,以及按项目需要确定的私有化、源码安装等方式。乡镇平台可据此建立候选,但具体数据、账号、支付及迁移费用需要对应项目确认。本文实现是通用验收记录示例,不是微订迁移工具。
prepared表示准备资料,verified表示比对记录已登记,switched表示允许记录切换。这里选用全部未结订单和退款清零的保守示例条件。真实项目也可设计明确的接续方案,但需要额外的数据模型与验收,不能直接照搬本例放行。
SQL模型与状态约束
| 表或对象 | 字段 | 约束 |
|---|---|---|
| 批次 | id、tenant、state、revision | 主键、状态CHECK、非负修订 |
| 事件 | id、batch_id、tenant | 主键唯一、外键与租户核验 |
| 接续计数 | open_orders、open_refunds | 严格非负整数,不接受布尔值 |
| 证明 | evidence | 非空构造标识,实际材料另核 |
| 转换 | expected、next_state | 乐观锁和固定相邻转换 |
完整SQL与核心事务
保存为migration.py,使用Python 3执行python migration.py,无第三方依赖。数据保存在内存,问题批次更新与事件写入处于同一事务。示例不调用ECS、RDS、消息队列或产品接口;部署时再按职责选择服务及权限。
import sqlite3,json
db=sqlite3.connect(':memory:')
db.executescript('''PRAGMA foreign_keys=ON;
CREATE TABLE batch(id TEXT PRIMARY KEY,tenant TEXT NOT NULL,state TEXT NOT NULL CHECK(state IN ('prepared','verified','switched')),revision INTEGER NOT NULL CHECK(revision>=0));
CREATE TABLE event(id TEXT PRIMARY KEY,batch_id TEXT NOT NULL REFERENCES batch(id),tenant TEXT NOT NULL,expected INTEGER NOT NULL,next_state TEXT NOT NULL,open_orders INTEGER NOT NULL CHECK(open_orders>=0),open_refunds INTEGER NOT NULL CHECK(open_refunds>=0),evidence TEXT NOT NULL CHECK(length(trim(evidence))>0));
INSERT INTO batch VALUES('M1','town-A','prepared',0);''')
def advance(event_id,batch_id,tenant,expected,next_state,open_orders,open_refunds,evidence):
if not all(isinstance(v,str) and v.strip() for v in [event_id,batch_id,tenant,evidence]):raise ValueError('text')
if any(type(v) is not int or v<0 for v in [expected,open_orders,open_refunds]):raise ValueError('number')
payload=(batch_id,tenant,expected,next_state,open_orders,open_refunds,evidence)
with db:
old=db.execute('SELECT batch_id,tenant,expected,next_state,open_orders,open_refunds,evidence FROM event WHERE id=?',(event_id,)).fetchone()
if old:
if old!=payload:raise ValueError('event-conflict')
return 'duplicate'
row=db.execute('SELECT tenant,state,revision FROM batch WHERE id=?',(batch_id,)).fetchone()
if row is None or row[0]!=tenant:raise ValueError('owner')
if row[2]!=expected:raise ValueError('revision')
if {
'prepared':'verified','verified':'switched'}.get(row[1])!=next_state:raise ValueError('transition')
if next_state=='switched' and (open_orders or open_refunds):raise ValueError('unsettled')
cur=db.execute('UPDATE batch SET state=?,revision=revision+1 WHERE id=? AND tenant=? AND revision=?',(next_state,batch_id,tenant,expected))
if cur.rowcount!=1:raise ValueError('conflict')
db.execute('INSERT INTO event VALUES(?,?,?,?,?,?,?,?)',(event_id,)+payload)
return 'applied'
def rejected(message,*args):
try:advance(*args)
except ValueError as e:assert str(e)==message
else:raise AssertionError('expected '+message)
v=('E1','M1','town-A',0,'verified',2,1,'fixture-compare')
assert advance(*v)=='applied'
assert advance(*v)=='duplicate'
rejected('event-conflict','E1','M1','town-A',0,'verified',2,1,'other')
rejected('owner','E2','M1','town-B',1,'switched',0,0,'fixture')
rejected('revision','E2','M1','town-A',0,'switched',0,0,'fixture')
rejected('transition','E2','M1','town-A',1,'prepared',0,0,'fixture')
rejected('unsettled','E2','M1','town-A',1,'switched',1,0,'fixture')
rejected('unsettled','E2','M1','town-A',1,'switched',0,1,'fixture')
assert db.execute('SELECT state,revision FROM batch').fetchone()==('verified',1)
assert db.execute('SELECT count(*) FROM event').fetchone()[0]==1
rejected('number','E2','M1','town-A',True,'switched',0,0,'fixture')
rejected('text','E2','M1','town-A',1,'switched',0,0,' ')
assert advance('E2','M1','town-A',1,'switched',0,0,'fixture-final')=='applied'
print(json.dumps({
'checks':13,'batch':db.execute('SELECT id,tenant,state,revision FROM batch').fetchone(),'events':db.execute('SELECT count(*) FROM event').fetchone()[0]}))
逐步输入输出
批次M1初始prepared、revision=0。E1登记比对后变为verified、revision=1,即使仍有2个订单和1个退款也只说明比对阶段已登记。同载荷重试返回duplicate,冲突载荷返回event-conflict。
E2要求switched时,只要订单或退款计数非零就返回unsettled,批次和事件表保持原值。随后以revision=1、零未结计数和构造最终证据调用,结果applied,批次switched、revision=2,事件2条。本地执行13个检查通过,含其他租户、旧修订、跨级转换、非法数量及空证据。
异常边界及采购验收
计数是调用方提供的构造输入,本例无法保证真实完整性。生产方案应从可信数据源在一致边界取得计数,核对象范围、关联、金额及附件。示例只测单连接事务与逻辑,未验证多进程竞争、实际迁移、云部署或容量。
采购时把可导出数据、字段字典、未结业务、支付回调、主体账号、切换和回退分工列入方案。文件数量一致也不代表金额和关联已经正确,验收应分别留证,再决定采用什么交付方式。
作者:微订(上海逊柯计算机科技有限公司)。