配送费从300分改到500分后,刚创建的旧订单该收多少?如果结算时只读“当前配送费”,历史订单就会随配置改变。问题不仅是金额:新版本禁止取消,也不应自动改变旧订单创建时适用的取消规则。
下面用SQLite演示一种明确的语义:规则发布成不可修改的版本,订单创建时绑定规则版本并保存费用快照;后续操作使用绑定版本判断政策,再用订单自身版本检查是否发生了过期写入。所有金额和订单编号都是本地示例数据。
字段 | 含义 | 什么时候变化 |
|---|---|---|
rule.version | 一组费用与取消政策的发布版本 | 发布新政策时新增一行 |
orders.rule_version | 该订单适用的政策版本 | 本例创建后保持不变 |
orders.version | 订单当前状态的写入版本 | 每次有效状态变更后增加 |
规则版本解释“按什么政策处理”,订单版本解释“操作基于哪次状态”。用同一个version字段同时承担两件事,会让配置更新和订单接单互相干扰。费用另存到订单,是为了明确这笔业务当时确认的金额;历史政策仍通过外键保留。
rule的UPDATE和DELETE由触发器拒绝,修改政策只能插入新版本并切换active_rule。create_order在BEGIN IMMEDIATE之后读取当前版本与费用,再写入订单。本例以这个事务内的读取作为绑定点,不能让前端展示的旧报价直接覆盖服务端结果。
如果真实业务要求用户明确接受价格变化,应增加报价标识、有效期及重新确认流程。本例没有实现报价协商,重点是展示政策发布后旧订单仍能查到原版本。下面代码可直接保存为demo.py。
import sqlite3
from contextlib import closing
def publish(db, version, fee, can_cancel):
with db:
db.execute("INSERT INTO rule VALUES (?, ?, ?)",
(version, fee, int(can_cancel)))
db.execute("UPDATE active_rule SET version=? WHERE id=1", (version,))
def create_order(db, order_id):
with db:
db.execute("BEGIN IMMEDIATE")
version, fee = db.execute(
"SELECT r.version, r.fee_fen FROM active_rule a "
"JOIN rule r ON r.version=a.version WHERE a.id=1"
).fetchone()
db.execute("INSERT INTO orders VALUES (?, ?, ?, 'new', 1)",
(order_id, version, fee))
def transition(db, order_id, expected, action):
if action not in {"accept", "cancel"}:
raise ValueError("unknown action")
with db:
row = db.execute(
"SELECT r.can_cancel FROM orders o "
"JOIN rule r ON r.version=o.rule_version WHERE o.id=?",
(order_id,)
).fetchone()
if row is None:
raise ValueError("unknown order")
if action == "cancel" and not row[0]:
return "policy_denied"
state = "accepted" if action == "accept" else "cancelled"
changed = db.execute(
"UPDATE orders SET state=?, version=version+1 "
"WHERE id=? AND state='new' AND version=?",
(state, order_id, expected)
).rowcount
return "applied" if changed else "conflict"
def main():
with closing(sqlite3.connect(":memory:")) as db:
db.executescript("""
PRAGMA foreign_keys=ON;
CREATE TABLE rule (version INTEGER PRIMARY KEY, fee_fen INTEGER CHECK(fee_fen>=0),
can_cancel INTEGER CHECK(can_cancel IN (0,1)));
CREATE TRIGGER immutable_rule_update BEFORE UPDATE ON rule
BEGIN SELECT RAISE(ABORT, 'published rule is immutable'); END;
CREATE TRIGGER immutable_rule_delete BEFORE DELETE ON rule
BEGIN SELECT RAISE(ABORT, 'published rule is immutable'); END;
INSERT INTO rule VALUES (1, 300, 1);
CREATE TABLE active_rule (id INTEGER PRIMARY KEY, version INTEGER REFERENCES rule);
INSERT INTO active_rule VALUES (1, 1);
CREATE TABLE orders (id TEXT PRIMARY KEY, rule_version INTEGER REFERENCES rule,
fee_fen INTEGER, state TEXT, version INTEGER);
""")
create_order(db, "old")
publish(db, 2, 500, False)
create_order(db, "new")
rows = db.execute(
"SELECT id, rule_version, fee_fen FROM orders ORDER BY rowid"
).fetchall()
assert rows == [("old", 1, 300), ("new", 2, 500)]
assert transition(db, "old", 1, "cancel") == "applied"
assert transition(db, "new", 1, "cancel") == "policy_denied"
assert transition(db, "new", 1, "accept") == "applied"
assert transition(db, "new", 1, "accept") == "conflict"
try:
with db:
db.execute("UPDATE rule SET fee_fen=999 WHERE version=1")
except sqlite3.IntegrityError:
pass
else:
raise AssertionError("published rule was overwritten")
assert db.execute("SELECT fee_fen FROM rule WHERE version=1").fetchone()[0] == 300
print("snapshots:", rows)
print("old order cancellation: applied")
print("new order cancellation: policy_denied")
print("stale order version: conflict")
print("published rule mutation: rejected")
if __name__ == "__main__":
main()python demo.py本次在Python 3.12.10、SQLite 3.49.1下运行,实际输出为:
snapshots: [('old', 1, 300), ('new', 2, 500)]
old order cancellation: applied
new order cancellation: policy_denied
stale order version: conflict
published rule mutation: rejectedold在规则1生效时创建,因此保留300分且允许取消。new在规则2发布后创建,因此保存500分且取消返回policy_denied。接单操作将new的订单版本由1改成2,随后携带旧版本1的接单返回conflict,两个结果没有使用同一种“失败”解释。
最后主动执行一次对规则1的UPDATE,触发器抛出IntegrityError;代码又读取原值,确认仍为300分。这里验证的是本数据库连接所见的约束行为,不是权限系统或防篡改认证。
订单长期引用历史版本,就需要保留历史规则及解释它的执行逻辑。仅保存一串JSON而删除旧计算逻辑,仍可能无法重现当时结果。本例规则只有固定费用与取消开关,复杂计价还需记录距离、时段、取整方式等决定结果的输入。
本例串行执行发布、创建和操作,验证旧新订单差异以及过期版本拒绝,没有进行并发压力测试。SQLite的写锁安排也不能当成其他数据库的吞吐结论。事务隔离、锁等待和驱动影响行数语义都需要在目标数据库重新验证。
服务端接入时还要先校验操作者能否访问该区域和订单。本例省略HTTP层与账号系统,也没有实现操作幂等;重复的旧版本操作得到conflict,不能把它误称为“自动返回首次成功结果”。需要这种能力时,应另存有作用域的请求键、输入与结果。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。