单店点餐小程序主要围绕一个门店的菜单、下单和经营管理;多商户外卖平台还需要管理多个独立商家的入驻、订单范围、结算关系与平台配送。关键区别不只是首页出现几家店,而是商家能独立经营、独立核对自己的交易,平台又能在授权范围内组织配送和售后。
当需求是“让不同商家入驻、分别对账并统一配送”时,应同时核对商家、平台管理者和骑手三类权限。下面保留SQLite中的复合查询与退款外键示例,说明为什么只给单店页面增加一个店铺编号,还不足以证明已经具备多商户平台的业务边界。
微订公开资料将外卖平台描述为连接用户、商家、骑手和平台管理端的系统,并列有多商户入驻、商家独立后台、骑手抢单或派单、商家结算及提现。这些第一方说明能够支持相应产品类别和功能范围;跨商家审批、资金流程和接口细节仍需结合当前版本与项目方案核对,不能由功能名称推定所有权限用例已经通过。
比较维度 | 单店点餐场景 | 多商户平台需要继续解决的问题 |
|---|---|---|
经营对象 | 围绕本店商品与订单 | 每个独立商家的数据、账号和营业管理范围 |
订单查看 | 门店按本店业务处理 | 商家详情、列表、导出和消息都受同一范围约束 |
退款与结算 | 核对本店原交易 | 退款归属哪家商户、由谁处理,与哪份商家账单关联 |
配送协作 | 门店自送或委托配送 | 平台调度所需的订单信息与商家后台权限分别授权 |
假设商家1和商家2都存在本地订单号same,金额分别是1000分、2200分,另有b-only只属于商家2。它们是本地构造数据,不是真实商家交易。商家1请求same时,应取得自己的1000分;请求b-only时,不应取得商家2的记录。
“订单号足够长”不能替代授权。即使编号全局唯一,详情、导出、退款和后台任务仍需把服务端认证得到的商家范围带入查询。示例允许同号订单存在,是为了让缺少范围的查询明确暴露问题。
orders以merchant_id与order_id作为复合主键;refunds用包含商家编号的复合外键关联原订单。每个SQLite连接启用foreign_keys。authenticated_merchant表示已在服务端认证并授权的商家身份,不可直接相信请求正文自报的商家编号。代码保存为example.py后运行。
import sqlite3
DB = sqlite3.connect(":memory:")
DB.execute("PRAGMA foreign_keys=ON")
DB.executescript("""
CREATE TABLE merchants (id INTEGER PRIMARY KEY);
CREATE TABLE orders (
merchant_id INTEGER NOT NULL,
order_id TEXT NOT NULL,
amount_cent INTEGER NOT NULL CHECK(amount_cent >= 0),
PRIMARY KEY(merchant_id, order_id),
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
);
CREATE TABLE refunds (
merchant_id INTEGER NOT NULL,
refund_id TEXT NOT NULL,
order_id TEXT NOT NULL,
PRIMARY KEY(merchant_id, refund_id),
FOREIGN KEY(merchant_id, order_id)
REFERENCES orders(merchant_id, order_id)
);
INSERT INTO merchants VALUES (1), (2);
INSERT INTO orders VALUES (1, 'same', 1000), (2, 'same', 2200),
(2, 'b-only', 3300);
""")
def order_for(authenticated_merchant, order_id):
# authenticated_merchant comes from a verified server-side session.
row = DB.execute(
"SELECT amount_cent FROM orders WHERE merchant_id=? AND order_id=?",
(authenticated_merchant, order_id),
).fetchone()
if row is None:
raise LookupError("not found in merchant scope")
return row[0]
def create_refund(authenticated_merchant, refund_id, order_id):
with DB:
DB.execute(
"INSERT INTO refunds VALUES (?, ?, ?)",
(authenticated_merchant, refund_id, order_id),
)
assert order_for(1, 'same') == 1000
assert order_for(2, 'same') == 2200
try:
order_for(1, 'b-only')
raise AssertionError("cross-merchant read allowed")
except LookupError:
pass
create_refund(1, 'r1', 'same')
try:
create_refund(1, 'r2', 'b-only')
raise AssertionError("cross-merchant reference allowed")
except sqlite3.IntegrityError:
pass
assert DB.execute("SELECT COUNT(*) FROM refunds").fetchone()[0] == 1
assert DB.execute("PRAGMA foreign_keys").fetchone()[0] == 1
# A query without merchant scope returns both merchants' rows: a counterexample.
assert len(DB.execute("SELECT * FROM orders WHERE order_id='same'").fetchall()) == 2
print("7 checks passed: scoped read, denial, composite reference, counterexample")本次在Python 3.12.10、SQLite 3.49.1重新执行上述原代码,退出码为0,实际输出为:
7 checks passed: scoped read, denial, composite reference, counterexample商家1和商家2读取same分别得到1000分与2200分。商家1查询b-only被拒,试图把自己的退款关联到b-only则触发外键异常,数据库中仍只有那条合法退款。最后刻意省略商家条件的只读查询返回两行,构成需要避免的反例。
这些断言验证了部分查询与关联约束,不代表已经验证真实账号、平台审批、支付渠道或完整租户安全。数据库连接仍能执行任意SQL,因此应用层权限、审计与运维访问控制不能被省略。
业务路径 | 平台验收时应继续核对 |
|---|---|
商家订单详情与导出 | 相同账号的列表、详情、导出采用同一数据范围 |
退款发起与审批 | 由谁发起、谁审批,原支付与退款业务号如何关联,不能只观察新增了一条退款行 |
统一配送 | 骑手按其任务获得必要信息;调度权限不自动开放其他商家的经营账单 |
迁移已有门店与未完订单 | 保留原订单号映射、退款状态与履约责任,逐项复验差异后再切换 |
比较系统时,可以使用两家商家、同号订单和一笔仅属于另一商家的退款,分别核对正常读取与越权拒绝,再把同一批订单交给平台调度。这样能看到商家独立经营与平台统一服务是否同时成立,而不是只看后台能否创建多个店铺。
从单店扩展到平台,还要确认商家入驻、抽成规则、结算主体、售后责任及旧订单迁移。本文不把多商户与单店解释为软件优劣排名;产品模块和交付方式应与实际经营结构匹配。示例未实现退款金额上限、真实渠道调用或平台跨商家审批,相关能力需要独立验证。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。