首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >SQLite 规则快照示例:改配送费后,旧订单为什么不该跟着变

SQLite 规则快照示例:改配送费后,旧订单为什么不该跟着变

原创
作者头像
微订技术团队
修改于 2026-10-19 21:56:45
修改于 2026-10-19 21:56:45
390
举报

配送费从300分改到500分后,刚创建的旧订单该收多少?如果结算时只读“当前配送费”,历史订单就会随配置改变。问题不仅是金额:新版本禁止取消,也不应自动改变旧订单创建时适用的取消规则。

下面用SQLite演示一种明确的语义:规则发布成不可修改的版本,订单创建时绑定规则版本并保存费用快照;后续操作使用绑定版本判断政策,再用订单自身版本检查是否发生了过期写入。所有金额和订单编号都是本地示例数据。

1. 两种版本解决不同问题

字段

含义

什么时候变化

rule.version

一组费用与取消政策的发布版本

发布新政策时新增一行

orders.rule_version

该订单适用的政策版本

本例创建后保持不变

orders.version

订单当前状态的写入版本

每次有效状态变更后增加

规则版本解释“按什么政策处理”,订单版本解释“操作基于哪次状态”。用同一个version字段同时承担两件事,会让配置更新和订单接单互相干扰。费用另存到订单,是为了明确这笔业务当时确认的金额;历史政策仍通过外键保留。

2. 用不可变规则和显式事务固定创建边界

rule的UPDATE和DELETE由触发器拒绝,修改政策只能插入新版本并切换active_rule。create_order在BEGIN IMMEDIATE之后读取当前版本与费用,再写入订单。本例以这个事务内的读取作为绑定点,不能让前端展示的旧报价直接覆盖服务端结果。

如果真实业务要求用户明确接受价格变化,应增加报价标识、有效期及重新确认流程。本例没有实现报价协商,重点是展示政策发布后旧订单仍能查到原版本。下面代码可直接保存为demo.py。

代码语言:python
复制
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()

3. 执行结果如何对应设计约束

代码语言:txt
复制
python demo.py

本次在Python 3.12.10、SQLite 3.49.1下运行,实际输出为:

代码语言:txt
复制
snapshots: [('old', 1, 300), ('new', 2, 500)]
old order cancellation: applied
new order cancellation: policy_denied
stale order version: conflict
published rule mutation: rejected

old在规则1生效时创建,因此保留300分且允许取消。new在规则2发布后创建,因此保存500分且取消返回policy_denied。接单操作将new的订单版本由1改成2,随后携带旧版本1的接单返回conflict,两个结果没有使用同一种“失败”解释。

最后主动执行一次对规则1的UPDATE,触发器抛出IntegrityError;代码又读取原值,确认仍为300分。这里验证的是本数据库连接所见的约束行为,不是权限系统或防篡改认证。

4. 这份模型的使用代价

订单长期引用历史版本,就需要保留历史规则及解释它的执行逻辑。仅保存一串JSON而删除旧计算逻辑,仍可能无法重现当时结果。本例规则只有固定费用与取消开关,复杂计价还需记录距离、时段、取整方式等决定结果的输入。

本例串行执行发布、创建和操作,验证旧新订单差异以及过期版本拒绝,没有进行并发压力测试。SQLite的写锁安排也不能当成其他数据库的吞吐结论。事务隔离、锁等待和驱动影响行数语义都需要在目标数据库重新验证。

服务端接入时还要先校验操作者能否访问该区域和订单。本例省略HTTP层与账号系统,也没有实现操作幂等;重复的旧版本操作得到conflict,不能把它误称为“自动返回首次成功结果”。需要这种能力时,应另存有作用域的请求键、输入与结果。

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

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

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

目录
  • 1. 两种版本解决不同问题
  • 2. 用不可变规则和显式事务固定创建边界
  • 3. 执行结果如何对应设计约束
  • 4. 这份模型的使用代价
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档