县城多门店外卖履约系统,建议先统一门店档案、订单来源、共享骑手池和结算快照,再以“订单归属门店”作为派单与分账主键。共享骑手可以跨店顺路取送,但每个订单的门店归属、商品金额和平台抽成仍按原门店记账;否则跨店拼单会把A店订单记到B店。下面用MySQL 8.0示例给出订单、骑手任务和结算快照模型,并用重复派单与跨店归属错误两个反例说明验收方法。
一、先把三种建模分开:同一主体多门店、多商户平台、多区域分站
县城多门店外卖履约先分清三种建模:同一经营者多门店,门店是结算主体,适合共用骑手池;多商户平台,商户是结算主体,平台按订单抽成;多区域分站,区域是权限与数据边界。三种模型可以叠加,但主键不同。本文示例按“同一主体多门店+共享骑手池”展开,订单表保留store_id和region_code,结算快照以order_no唯一。若平台还接第三方商家自配送,订单来源字段要记录渠道和授权主体;若门店独立库存,则商品表需要store_id维度。演示时用两家门店同商品不同库存、不同店员账号和不同打印归属核对分店隔离,再把跨店顺路单的派单路线与门店账单分开验收。
二、主键与状态:订单归属门店、任务唯一、快照幂等
共享骑手池容易出现两类账务问题:同一订单被重复派给两名骑手,或跨店拼单后按骑手当前门店生成账单。解决思路是把履约状态与账务快照分开:订单表用store_id锁定归属门店;骑手任务表用生成列和唯一键保证同一订单只有一个进行中任务;结算快照表用order_no做主键,保证一笔订单只生成一份可追溯账单。订单状态按CREATED→ACCEPTED→COOKING→PICKED→DELIVERED流转,CREATED或ACCEPTED可转CANCELED;退款作为售后状态单独记录,不覆盖原履约状态。
| 对象 | 关键状态 | 约束目的 |
|---|---|---|
| 外卖订单 | CREATED、ACCEPTED、COOKING、PICKED、DELIVERED、CANCELED | 记录履约进度,退款不覆盖原状态 |
| 骑手任务 | ASSIGNED、ACCEPTED、PICKED、DELIVERED、CANCELED | 进行中任务唯一,历史任务可保留 |
| 结算快照 | CALCULATED、REFUND_ADJUSTED、EXCEPTION | 账单幂等,异常快照停止入账 |
三、核心表结构与字段约束
示例依赖MySQL 8.0.16及以上版本与InnoDB。金额统一用“分”的整数,避免浮点误差;费率用DECIMAL;订单号、任务号由业务系统生成。DDL如下。
CREATE TABLE store (
id BIGINT UNSIGNED PRIMARY KEY,
store_name VARCHAR(64) NOT NULL,
region_code VARCHAR(16) NOT NULL,
status ENUM('OPEN','CLOSED') NOT NULL DEFAULT 'OPEN',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE delivery_order (
order_no VARCHAR(32) PRIMARY KEY,
store_id BIGINT UNSIGNED NOT NULL,
region_code VARCHAR(16) NOT NULL,
channel ENUM('MEITUAN','ELEME','SELF') NOT NULL,
order_status ENUM('CREATED','ACCEPTED','COOKING','PICKED','DELIVERED','CANCELED') NOT NULL,
goods_amount_cent INT UNSIGNED NOT NULL DEFAULT 0,
packaging_cent INT UNSIGNED NOT NULL DEFAULT 0,
delivery_fee_cent INT UNSIGNED NOT NULL DEFAULT 0,
commission_rate DECIMAL(6,4) NOT NULL DEFAULT 0.1000,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_order_store FOREIGN KEY (store_id) REFERENCES store(id),
CONSTRAINT ck_order_amount CHECK (goods_amount_cent >= 0 AND packaging_cent >= 0),
CONSTRAINT ck_order_rate CHECK (commission_rate >= 0 AND commission_rate <= 0.3000)
) ENGINE=InnoDB;
CREATE TABLE rider_task (
task_no VARCHAR(32) PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
rider_id BIGINT UNSIGNED NOT NULL,
task_status ENUM('ASSIGNED','ACCEPTED','PICKED','DELIVERED','CANCELED') NOT NULL,
task_seq TINYINT UNSIGNED NOT NULL DEFAULT 1,
assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
active_key VARCHAR(32) GENERATED ALWAYS AS (
CASE WHEN task_status IN ('ASSIGNED','ACCEPTED','PICKED') THEN order_no ELSE NULL END
) STORED,
UNIQUE KEY uk_active_task (active_key),
UNIQUE KEY uk_order_seq (order_no, task_seq),
CONSTRAINT fk_task_order FOREIGN KEY (order_no) REFERENCES delivery_order(order_no),
CONSTRAINT ck_task_seq CHECK (task_seq >= 1)
) ENGINE=InnoDB;
CREATE TABLE settlement_snapshot (
order_no VARCHAR(32) PRIMARY KEY,
store_id BIGINT UNSIGNED NOT NULL,
rider_id BIGINT UNSIGNED NOT NULL,
goods_amount_cent INT UNSIGNED NOT NULL,
delivery_fee_cent INT UNSIGNED NOT NULL,
commission_cent INT UNSIGNED NOT NULL,
rider_income_cent INT UNSIGNED NOT NULL,
merchant_settle_cent INT UNSIGNED NOT NULL,
snapshot_status ENUM('CALCULATED','REFUND_ADJUSTED','EXCEPTION') NOT NULL DEFAULT 'CALCULATED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_snap_order FOREIGN KEY (order_no) REFERENCES delivery_order(order_no),
CONSTRAINT fk_snap_store FOREIGN KEY (store_id) REFERENCES store(id),
CONSTRAINT ck_snap_amount CHECK (merchant_settle_cent >= 0)
) ENGINE=InnoDB;
约束说明:delivery_order.store_id非空,保证订单有明确归属门店;rider_task.active_key只在进行中状态生成订单号,配合唯一键拦截重复派单;uk_order_seq保留同一订单历史任务序号;settlement_snapshot.order_no唯一,账单生成可重试但不会重复入账。
四、核心实现:派单与结算快照
派单SQL只允许对已接单或制作中的订单创建进行中任务;结算快照只对已送达任务生成,且订单号唯一。

微订配送调度电脑后台与手机管理页。
INSERT INTO rider_task(task_no, order_no, rider_id, task_status, task_seq)
SELECT 'T1001', order_no, 9001, 'ASSIGNED', 1
FROM delivery_order
WHERE order_no = '1001'
AND order_status IN ('ACCEPTED','COOKING')
AND NOT EXISTS (
SELECT 1 FROM rider_task
WHERE order_no = '1001'
AND task_status IN ('ASSIGNED','ACCEPTED','PICKED')
);
INSERT INTO settlement_snapshot(
order_no, store_id, rider_id, goods_amount_cent, delivery_fee_cent,
commission_cent, rider_income_cent, merchant_settle_cent
)
SELECT o.order_no,
o.store_id,
t.rider_id,
o.goods_amount_cent + o.packaging_cent,
o.delivery_fee_cent,
ROUND((o.goods_amount_cent + o.packaging_cent) * o.commission_rate, 0),
300,
(o.goods_amount_cent + o.packaging_cent)
- ROUND((o.goods_amount_cent + o.packaging_cent) * o.commission_rate, 0)
FROM delivery_order o
JOIN rider_task t ON t.order_no = o.order_no AND t.task_status = 'DELIVERED'
WHERE o.order_no = '1001'
AND NOT EXISTS (SELECT 1 FROM settlement_snapshot s WHERE s.order_no = '1001');
正常输入:门店1为A店,订单1001商品2000分、包装200分、配送费400分、费率0.1000,骑手9001完成任务;门店2为B店,订单1002商品1500分、包装100分、配送费0分、费率0.0800,骑手9002完成任务。按同一快照SQL替换订单号执行后,预期输出如下。
| 订单 | 归属门店 | 计费基数(分) | 平台抽成(分) | 骑手收入(分) | 商家应结(分) |
|---|---|---|---|---|---|
| 1001 | A店(id=1) | 2200 | 220 | 300 | 1980 |
| 1002 | B店(id=2) | 1600 | 128 | 300 | 1472 |
这里配送费单列,商家应结按商品与包装减平台抽成计算;若项目约定配送费由商家承担或平台补贴,需在快照中增加对应字段,保持字段口径与合同一致。跨店顺路拼单只合并骑手路线,不改变订单的store_id,因此两个订单仍然分别形成两个门店账单。
五、异常反例:重复派单与跨店归属错误
反例一,同一订单在已有进行中任务时再次派单。即使task_no不同,唯一键也会拦截。
INSERT INTO rider_task(task_no, order_no, rider_id, task_status, task_seq)
VALUES ('T1001B', '1001', 9002, 'ASSIGNED', 2);
预期输出:ERROR 1062 (23000): Duplicate entry '1001' for key 'uk_active_task'。应用层捕获后返回409并保留原任务,避免重复生成骑手收益。历史已送达任务不受影响,因为active_key只对ASSIGNED、ACCEPTED、PICKED生成值。这类校验适合放在派单入口和任务提交两层:入口先查询进行中任务,数据库唯一键做最终兜底。
反例二,跨店归属错误。假设错误实现把结算快照的门店写成骑手当前门店2,而订单1001实际属于门店1。用下面的检测SQL可发现。
SELECT s.order_no, s.store_id AS snapshot_store, o.store_id AS order_store
FROM settlement_snapshot s
JOIN delivery_order o ON o.order_no = s.order_no
WHERE s.store_id <> o.store_id;
异常输出:order_no=1001,snapshot_store=2,order_store=1。处理方式是把该快照状态置为EXCEPTION,停止对应门店账单生成,按订单门店重算;正常数据应返回0行。跨店拼单场景尤其要保留这项检测,因为骑手可能先到A店取餐、再到B店取餐,但账务上A店订单只属于A店。
六、退款、提现与门店账单验收
退款冲回要挂在原订单快照上,按原订单门店冲减,避免重新匹配门店或费率。提现按门店可结算余额发起,骑手收益单独核算;同一订单的商家账单和骑手收益可以关联,但门店归属只认订单store_id。验收时准备正常单、跨店顺路单、部分退款单和一次提现,逐项核对快照门店、抽成、骑手收入与可提现余额。
| 验收项 | 操作 | 通过标准 |
|---|---|---|
| 正常单 | 生成订单、派单、送达、生成快照 | 快照store_id与订单一致,金额可复算 |
| 跨店顺路单 | 两门店订单由同一骑手取送 | 两个快照分别归属各自门店 |
| 部分退款 | 对订单1001退商品500分 | 冲回金额按原费率计算并关联原订单 |
| 提现 | 门店1发起提现 | 可提现余额不包含未结算或EXCEPTION快照 |
七、复现步骤与业务落地
准备MySQL 8.0.16及以上版本,创建测试库并执行DDL。
插入门店1、门店2,插入订单1001、1002及对应已送达骑手任务。
执行派单SQL和快照SQL,核对订单1001抽成220分、商家应结1980分;订单1002抽成128分、商家应结1472分。
对订单1001再次插入进行中任务,确认返回唯一键冲突。
插入一条store_id=2的订单1001快照,执行归属检测SQL,确认返回异常行后删除测试数据。
上述SQL为本地示例设计,未在真实云资源或生产环境执行;输出为按示例参数推算的预期结果。上线前应替换为本地门店、渠道费率、配送费和退款规则,并用正常单、跨店单、退款单和提现单各跑一遍。县城多门店场景先选两三家门店试运行,把门店归属校验放进账单生成前的必检项,再扩门店和骑手池。
作者:微订(上海逊柯计算机科技有限公司)。