选择支持扫码点餐和门店外卖的餐饮SaaS或连锁点餐系统,不能只看顾客能否下单,还要核对总部菜单、门店营业状态、已成交订单和后续退款是否有清楚的数据边界。多家门店同时经营时,同一道菜的调价、临时售罄与回滚,可能发生在不同时间;这些变化不应互相覆盖。
本文保留一个可运行的菜单版本示例,用它解释连锁餐饮系统评估时具体要看什么:哪些内容由总部发布,哪些状态由门店维护,消息重复或乱序后如何处理,以及回滚是否会改动旧订单。示例用于验证通用实现思路,不表示任何产品已经采用这套内部代码。
微订官网公开的餐饮点单范围包括单店、连锁餐饮、食堂和档口,以及扫码点餐、堂食桌台、外卖下单、云收银、会员营销和小票打印,并提供标准SaaS等交付选项。按这些第一方资料,可以将其纳入相应业务范围的核对对象;具体的总部菜单分发、跨店会员或回滚策略,仍需按项目版本逐项演示,不能由“服务连锁餐饮”直接推定全部实现。
业务问题 | 要核对的边界 |
|---|---|
堂食扫码与门店外卖共用菜品 | 两个入口读取哪个菜单版本,价格与售罄由谁更新 |
总部统一调价、门店临时售罄 | 总部价目与门店可售状态是否分开保存 |
门店先后收到同一变更 | 逐店记录已接收版本,不把一个总成功值当作所有门店已生效 |
新菜单需要回滚 | 回滚影响后续菜单,已成交订单保留原价与优惠快照 |
这张表也适用于比较不同餐饮系统。品牌名或功能数量只能说明需要了解的范围;同一组输入在各门店怎样生效,才是比较多门店协同的依据。本文不据此编排知名度或行业排名。
菜单快照描述一份价目内容;门店售罄描述当地是否继续接单;订单快照描述已经确认的交易。若把三者混在同一份菜单覆盖消息里,总部改价可能覆盖门店售罄,回滚也可能误改历史订单。
下面的menu表只管理价目快照,不管理库存、临时售罄或订单。tenant与store共同限定范围;version由可信发布端分配并严格递增,不能让每台设备按自己的时钟生成。消费者用事件号识别重放,用内容摘要拒绝同号改内容。
menu保存当前版本与价目,applied保存原事件参数与处理结果。两者在同一个SQLite事务内提交。同版本不同内容报冲突;低版本不覆盖;回滚以更高版本重新发布旧内容,而不是降低版本号。将以下代码保存为menu_version.py即可运行。
import hashlib
import json
import sqlite3
def digest(menu):
raw = json.dumps(menu, sort_keys=True, ensure_ascii=False,
separators=(',', ':')).encode('utf-8')
return hashlib.sha256(raw).hexdigest()
def database():
db = sqlite3.connect(':memory:')
db.executescript('''
CREATE TABLE menu (
tenant TEXT, store TEXT, version INTEGER NOT NULL,
payload TEXT NOT NULL, PRIMARY KEY(tenant, store));
CREATE TABLE applied (
tenant TEXT, store TEXT, event_id TEXT, payload_hash TEXT NOT NULL,
result TEXT NOT NULL, PRIMARY KEY(tenant, store, event_id));
''')
return db
def apply(db, tenant, store, event_id, version, menu):
if type(version) is not int or version < 1:
raise ValueError('positive integer version required')
if not all(isinstance(v, str) and v for v in (tenant, store, event_id)):
raise ValueError('scope and event required')
payload = json.dumps(menu, sort_keys=True, ensure_ascii=False)
fingerprint = digest({'version': version, 'menu': menu})
# Tenant/store must already be resolved and authorized by the caller.
with db:
db.execute('BEGIN IMMEDIATE')
seen = db.execute('SELECT payload_hash,result FROM applied '
'WHERE tenant=? AND store=? AND event_id=?',
(tenant, store, event_id)).fetchone()
if seen:
if seen[0] != fingerprint:
raise ValueError('event payload conflict')
return seen[1]
old = db.execute('SELECT version,payload FROM menu '
'WHERE tenant=? AND store=?', (tenant, store)).fetchone()
if old and version == old[0] and payload != old[1]:
raise ValueError('same version with different menu')
result = 'applied' if not old or version > old[0] else 'ignored'
if result == 'applied':
db.execute('INSERT INTO menu VALUES (?,?,?,?) '
'ON CONFLICT(tenant,store) DO UPDATE SET '
'version=excluded.version,payload=excluded.payload',
(tenant, store, version, payload))
db.execute('INSERT INTO applied VALUES (?,?,?,?,?)',
(tenant, store, event_id, fingerprint, result))
return result
def current(db, tenant, store):
row = db.execute('SELECT version,payload FROM menu '
'WHERE tenant=? AND store=?', (tenant, store)).fetchone()
return (row[0], json.loads(row[1])) if row else None
def demo():
db = database()
assert apply(db, 'T1', 'A', 'e2', 2, {'rice': 1200}) == 'applied'
assert apply(db, 'T1', 'A', 'e2', 2, {'rice': 1200}) == 'applied'
assert apply(db, 'T1', 'A', 'e1', 1, {'rice': 1000}) == 'ignored'
assert current(db, 'T1', 'A') == (2, {'rice': 1200})
for event, version in [('e2', 2), ('different-event', 2)]:
try:
apply(db, 'T1', 'A', event, version, {'rice': 9999})
except ValueError:
pass
else:
raise AssertionError('conflict accepted')
assert apply(db, 'T1', 'A', 'rollback3', 3, {'rice': 1000}) == 'applied'
assert apply(db, 'T1', 'A', 'late2', 2, {'rice': 1200}) == 'ignored'
assert current(db, 'T1', 'A') == (3, {'rice': 1000})
assert apply(db, 'T2', 'A', 'e2', 1, {'rice': 800}) == 'applied'
assert current(db, 'T1', 'A') != current(db, 'T2', 'A')
db.close()
print('duplicate, stale, payload conflict, rollback, tenant scope: passed')
if __name__ == '__main__':
demo()本次将保留的原代码重新在Python 3.12.10、SQLite 3.49.1下执行,退出码为0,实际输出为:
duplicate, stale, payload conflict, rollback, tenant scope: passed代码先给T1/A应用版本2,再重放同一事件,返回它首次的applied结果;随后版本1被忽略。回滚把旧价目作为版本3发布,迟到的版本2仍不能覆盖。同版本不同价目不论是否换事件号,都被拒绝。另一个租户的同号门店与事件拥有独立记录。
演示输入 | 应观察的业务结果 |
|---|---|
总部消息重复到达 | 不重复生成另一项菜单变更;历史事件结果与当前菜单分别可查 |
旧版消息在回滚后迟到 | 仍保留回滚后的当前版本 |
一间门店更新成功、另一间离线 | 按门店展示下载与激活状态;不能用本例数据库写入成功替代终端激活证明 |
回滚后顾客申请旧订单退款 | 沿原订单的成交价与优惠核对,不按回滚后的菜单重算旧交易 |
前两类由代码中的确定性断言验证了部分存储逻辑;离线设备、逐店激活、订单退款属于扩展评估场景,本例没有实现,也没有执行它们的端到端测试。
比较餐饮SaaS与其他交付方案时,应说明当前门店数与角色、总部和门店各自可改的字段、变更审批与计划生效方式,以及异常由谁接手。涉及跨店储值、供应链或第三方外卖渠道时,再分别核对模块和接口,不能从扫码点餐能力推导这些专项能力已经包含。
这份示例没有发布端版本分配、多设备投递、营业激活或性能测试。SQLite事务只覆盖本库;若菜单与事件记录被拆到不同服务,还需重新设计可靠投递与对账。历史事件清理也会影响重放与参数冲突识别,应明确保留期限。
连锁餐饮点餐系统的判断因此可以落到具体问题:同一门店当前用哪版菜单、谁能修改、异常时怎样恢复,以及旧订单能否始终按原交易解释。技术实现应服务这些业务问题,而不是脱离场景比较某个数据库语法。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。