堂食与外卖同时销售最后一份菜时,页面显示“有货”并不等于本次下单能成功。更难处理的是:预留已经超时释放,支付成功通知却随后才到。此时既不能忽略真实付款,也不能再次扣走后来顾客买到的那一份。
下面把问题收缩到单菜品预留:用SQLite保存可售量、预留和付款记录,分别验证不足拒绝、超时只释放一次,以及迟到付款进入待退款处置。示例金额统一为每份100分,时刻用整数表示,便于固定边界条件。
对象 | 本例保存什么 | 约束 |
|---|---|---|
stock | sku与available | 可售量不能小于0 |
hold | 数量、状态、deadline | held只能确认或释放一次 |
payment | 渠道付款号、预留、金额和结果 | 付款号重复时核对原参数 |
held表示本次仍占用可售量;confirmed表示这份占用已经确认,不再由超时任务释放;released表示占用已经返还。付款成功是一项独立事实,即使预留已经released,也要记录这笔付款并给出后续处置状态。
本例把now < deadline定义为有效期内,now >= deadline定义为到期。付款与清理任务使用相同边界,避免一边认为仍有效、另一边已经释放。这里的refund_pending只是一条待处理结果,没有发起真实退款。
release_due先尝试把到期且仍为held的记录改为released。只有影响行数为1,才把数量加回stock。重复执行时,状态条件不再满足,因此不会反复增加可售量。pay也调用同一释放逻辑,避免必须等清理任务运行才能发现已过期。
reserve把扣减可售量与建立预留放在同一事务中;即使预留编号重复导致插入失败,扣减也会回滚。下面是完整代码,保存为demo.py运行,不需要第三方包。
import sqlite3
from contextlib import closing
def reserve(db, hold_id, qty, deadline):
if type(qty) is not int or qty <= 0:
raise ValueError("positive integer quantity required")
with db:
changed = db.execute(
"UPDATE stock SET available=available-? "
"WHERE sku=1 AND available>=?", (qty, qty)
).rowcount
if changed != 1:
return False
db.execute("INSERT INTO hold VALUES (?, ?, 'held', ?)",
(hold_id, qty, deadline))
return True
def release_due(db, hold_id, now):
changed = db.execute(
"UPDATE hold SET state='released' "
"WHERE id=? AND state='held' AND deadline<=?",
(hold_id, now)
).rowcount
if changed:
qty = db.execute("SELECT qty FROM hold WHERE id=?",
(hold_id,)).fetchone()[0]
db.execute("UPDATE stock SET available=available+? WHERE sku=1",
(qty,))
return bool(changed)
def expire(db, hold_id, now):
with db:
return release_due(db, hold_id, now)
def pay(db, payment_id, hold_id, amount_fen, now):
# Assume signature, merchant and payment status were verified upstream.
with db:
previous = db.execute(
"SELECT hold_id, amount_fen, result FROM payment WHERE id=?",
(payment_id,)
).fetchone()
if previous:
if previous[:2] != (hold_id, amount_fen):
raise ValueError("payment ID payload conflict")
return previous[2]
row = db.execute("SELECT qty, state FROM hold WHERE id=?",
(hold_id,)).fetchone()
if row is None or type(amount_fen) is not int or amount_fen != row[0] * 100:
raise ValueError("unknown hold or amount mismatch")
if db.execute("SELECT 1 FROM payment WHERE hold_id=?",
(hold_id,)).fetchone():
raise ValueError("another payment needs reconciliation")
release_due(db, hold_id, now)
changed = db.execute(
"UPDATE hold SET state='confirmed' "
"WHERE id=? AND state='held' AND deadline>?",
(hold_id, now)
).rowcount
result = "confirmed" if changed else "refund_pending"
db.execute("INSERT INTO payment VALUES (?, ?, ?, ?)",
(payment_id, hold_id, amount_fen, result))
return result
def main():
with closing(sqlite3.connect(":memory:")) as db:
db.executescript("""
CREATE TABLE stock (sku INTEGER PRIMARY KEY, available INTEGER CHECK(available>=0));
CREATE TABLE hold (id TEXT PRIMARY KEY, qty INTEGER CHECK(qty>0),
state TEXT CHECK(state IN ('held','released','confirmed')), deadline INTEGER);
CREATE TABLE payment (id TEXT PRIMARY KEY, hold_id TEXT UNIQUE,
amount_fen INTEGER, result TEXT);
INSERT INTO stock VALUES (1, 1);
""")
assert reserve(db, "dine-in", 1, 100)
assert not reserve(db, "takeout", 1, 100)
assert expire(db, "dine-in", 100)
assert not expire(db, "dine-in", 101)
assert pay(db, "p1", "dine-in", 100, 102) == "refund_pending"
assert pay(db, "p1", "dine-in", 100, 103) == "refund_pending"
assert db.execute("SELECT available FROM stock").fetchone()[0] == 1
assert reserve(db, "takeout-2", 1, 200)
assert pay(db, "p2", "takeout-2", 100, 150) == "confirmed"
assert not expire(db, "takeout-2", 201)
assert db.execute("SELECT available FROM stock").fetchone()[0] == 0
assert db.execute("SELECT COUNT(*) FROM payment").fetchone()[0] == 2
print("last portion: second reservation rejected")
print("expiry release: exactly once")
print("late payment: refund_pending, stock unchanged")
print("timely payment: confirmed")
print("payment records: 2")
if __name__ == "__main__":
main()python demo.py本次使用Python 3.12.10、SQLite 3.49.1执行了上述代码,输出为:
last portion: second reservation rejected
expiry release: exactly once
late payment: refund_pending, stock unchanged
timely payment: confirmed
payment records: 2最后一份先被dine-in占用,另一个预留直接失败。到期释放执行两次,第二次返回False;随后两次p1付款处理都返回同一个refund_pending,可售量仍为1。这个断言同时检查了没有吞掉付款,也没有因为重复回调再次返还份数。
随后新预留在有效期内付款,状态变为confirmed。即使再执行超时清理,可售量仍为0。两次独立付款最终只有两条记录,重复回调没有增加第三条。
示例从“付款已经验证成功”开始。真实接口必须先验证签名、商户归属、原订单、金额和渠道状态;未知预留、金额不符或同一预留出现另一付款号,需要保留渠道原始证据并进入对账流程,不能把本例的异常返回当成完整支付处理方案。
数据库事务只能覆盖本库数据。待退款记录还需要可靠任务、渠道幂等编号及结果查询,不能在重试时新建另一笔退款。生产环境还要统一可信时钟,决定过期按收到通知的时刻还是渠道支付时刻裁决,本例选择处理时刻,只用于复现状态边界。
这里测试的是一组固定顺序的状态路径,没有模拟多连接同时付款,也没有测试真实支付、原料库存或加菜出餐。迁移到多菜品时,还需确定整单失败策略和锁定顺序,不能把单菜品事务直接当作跨服务原子性。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。