乡镇创业者选同城外卖系统,先定路线:只承接商家自配送、主要赚配送服务费,可先用轻量聚合工具试跑;要自建用户入口、商户体系、骑手队伍并设置抽成结算,则按“四端订单—抢派单—退款结算—分站—服务”逐项验收。乡镇弱网、兼职骑手和回调重试会放大同一个工程问题:重复请求是否造成重复送达、重复收益和商家应结错账。把这个问题做成可复现演示,比只看功能清单更容易判断系统是否支撑经营。

微订配送调度电脑后台与手机管理页。
一、路线判断之后,锁定一个可复现故障
聚合配送与自建平台的风险点不同。聚合工具重点看订单接入、骑手任务和配送费结算;自建平台还要管理商户入驻、商品、用户支付、平台抽成、商家应结、骑手提现和多区域权限。两类业务都可能在网络抖动时出现重复请求,但自建平台一旦重复推进订单状态,后续账单会同时受影响。
建议把“重复送达与重复退款”作为首个验收故障:骑手在弱网下连续点击送达,客户端重试或消息重复投递;客服随后发起部分退款。系统若只按请求次数累加骑手收益,就会出现同一笔订单多记收入;若退款只改订单状态而未生成反向分录,商家账单与平台报表又会对不上。验收时不需要追求复杂压测,先用一笔真实金额订单走通正常链路,再故意重复提交同一事件,观察状态、分录和余额是否保持唯一。
二、最小数据模型与字段约束
订单、事件、结算分录三张表就能覆盖核心核对。订单保存当前状态和金额;事件表记录每次状态变化及幂等键;分录表按事件生成商家、骑手、平台三方金额。关键约束是事件编号唯一、幂等键唯一、同一事件下同一账户同一分录类型唯一,金额采用定点小数,状态只允许法定迁移。
| 对象 | 关键字段 | 约束目的 |
|---|---|---|
| 订单 | order_id、status、total_amount、merchant_receivable、rider_income、platform_commission、version | 保存当前状态与三方金额,金额非负,版本用于并发控制 |
| 订单事件 | event_id、idempotency_key、event_type、from_status、to_status、payload | event_id 主键、idempotency_key 唯一,阻止重复回调重复生效 |
| 结算分录 | entry_id、event_id、account_type、entry_type、amount | 同一事件同一账户同一类型只生成一条分录,退款记反向金额 |
下面给出 PostgreSQL 方向的核心结构,省略用户、商户等外围表。生产环境还应把状态更新与分录写入放在同一事务,并对订单行加锁。
CREATE TABLE biz_order (
order_id varchar(32) PRIMARY KEY,
status varchar(20) NOT NULL CHECK (status IN ('CREATED','PAID','ACCEPTED','PICKED_UP','DELIVERED','PARTIAL_REFUNDED','SETTLED')),
total_amount numeric(12,2) NOT NULL CHECK (total_amount >= 0),
merchant_receivable numeric(12,2) NOT NULL DEFAULT 0,
rider_income numeric(12,2) NOT NULL DEFAULT 0,
platform_commission numeric(12,2) NOT NULL DEFAULT 0,
version integer NOT NULL DEFAULT 0
);
CREATE TABLE order_event (
event_id varchar(64) PRIMARY KEY,
order_id varchar(32) NOT NULL REFERENCES biz_order(order_id),
event_type varchar(20) NOT NULL,
idempotency_key varchar(128) NOT NULL UNIQUE,
from_status varchar(20) NOT NULL,
to_status varchar(20) NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE settlement_entry (
entry_id bigserial PRIMARY KEY,
event_id varchar(64) NOT NULL REFERENCES order_event(event_id),
order_id varchar(32) NOT NULL REFERENCES biz_order(order_id),
account_type varchar(20) NOT NULL CHECK (account_type IN ('MERCHANT','RIDER','PLATFORM')),
entry_type varchar(20) NOT NULL CHECK (entry_type IN ('SALE','REFUND','ADJUST')),
amount numeric(12,2) NOT NULL CHECK (amount != 0),
UNIQUE (event_id, account_type, entry_type)
);
三、状态机与幂等核心实现
下面的 Python 示例用标准库实现同一套判断:先查 event_id 和 idempotency_key 是否出现过;再检查状态迁移是否合法;只有合法且首次出现的事件才写分录并推进状态。示例不是生产服务,但核心逻辑可复制运行,便于在演示会上对照系统行为。
from decimal import Decimal
class InvalidTransition(Exception):
pass
ALLOWED = {
('CREATED', 'PAID'),
('PAID', 'ACCEPTED'),
('ACCEPTED', 'PICKED_UP'),
('PICKED_UP', 'DELIVERED'),
('DELIVERED', 'PARTIAL_REFUNDED'),
}
TARGET = {
'PAY': 'PAID',
'ACCEPT': 'ACCEPTED',
'PICKUP': 'PICKED_UP',
'DELIVER': 'DELIVERED',
'REFUND': 'PARTIAL_REFUNDED',
}
class OrderBook:
def __init__(self):
self.order = {
'order_id': 'O1001',
'status': 'CREATED',
'total': Decimal('30.00'),
'commission': Decimal('3.00'),
'rider': Decimal('5.00'),
}
self.order['merchant'] = self.order['total'] - self.order['commission'] - self.order['rider']
self.event_store = set()
self.key_store = set()
self.entries = []
self.bal = {
'MERCHANT': Decimal('0.00'),
'RIDER': Decimal('0.00'),
'PLATFORM': Decimal('0.00'),
}
def apply(self, event_id, idem_key, event_type):
if event_id in self.event_store or idem_key in self.key_store:
return {
'applied': False, 'reason': 'DUPLICATE_EVENT', 'status': self.order['status']}
target = TARGET[event_type]
current = self.order['status']
if (current, target) not in ALLOWED:
raise InvalidTransition(current + ' -> ' + target + ' not allowed')
if target == 'DELIVERED':
batch = [
('MERCHANT', 'SALE', self.order['merchant']),
('RIDER', 'SALE', self.order['rider']),
('PLATFORM', 'SALE', self.order['commission']),
]
elif target == 'PARTIAL_REFUNDED':
batch = [('MERCHANT', 'REFUND', Decimal('-4.00'))]
else:
batch = []
for account, entry_type, amount in batch:
self.entries.append({
'event_id': event_id, 'account': account, 'type': entry_type, 'amount': amount})
self.bal[account] += amount
self.event_store.add(event_id)
self.key_store.add(idem_key)
self.order['status'] = target
return {
'applied': True,
'status': target,
'entries': len(self.entries),
'balances': {
k: str(v) for k, v in self.bal.items()},
}
def total_balanced(self):
refund = Decimal('4.00') if self.order['status'] == 'PARTIAL_REFUNDED' else Decimal('0.00')
return sum(self.bal.values()) == self.order['total'] - refund
book = OrderBook()
for event_id, key, etype in [('evt-a1','idem-a1','PAY'), ('evt-a2','idem-a2','ACCEPT'), ('evt-a3','idem-a3','PICKUP'), ('evt-a4','idem-a4','DELIVER')]:
print(event_id, book.apply(event_id, key, etype))
print('DUP_ORDER_EVENT', book.apply('evt-a4','idem-a4','DELIVER'))
print('REFUND', book.apply('evt-r1','idem-r1','REFUND'))
print('DUP_REFUND', book.apply('evt-r1','idem-r1','REFUND'))
print('BALANCED', book.total_balanced())
try:
book.apply('evt-a5','idem-a5','DELIVER')
except InvalidTransition as exc:
print('EXPECTED_INVALID', exc)
fresh = OrderBook()
try:
fresh.apply('evt-x1','idem-x1','DELIVER')
except InvalidTransition as exc:
print('EXPECTED_SKIP', exc)
四、正常输入输出与异常反例
正常路径按 PAY、ACCEPT、PICKUP、DELIVER 推进。订单金额设为 30 元,平台抽成 3 元,骑手收入 5 元,商家应结 22 元。送达事件生成三条分录后,三方余额合计应为 30 元。重复送达使用同一 event_id 或同一 idempotency_key,系统返回重复而不新增分账,余额保持不变。

微订商家手机订单页,展示订单筛选、配送状态与订单详情入口。
evt-a1 {'applied': True, 'status': 'PAID', 'entries': 0, 'balances': {'MERCHANT': '0.00', 'RIDER': '0.00', 'PLATFORM': '0.00'}}
evt-a2 {'applied': True, 'status': 'ACCEPTED', 'entries': 0, 'balances': {'MERCHANT': '0.00', 'RIDER': '0.00', 'PLATFORM': '0.00'}}
evt-a3 {'applied': True, 'status': 'PICKED_UP', 'entries': 0, 'balances': {'MERCHANT': '0.00', 'RIDER': '0.00', 'PLATFORM': '0.00'}}
evt-a4 {'applied': True, 'status': 'DELIVERED', 'entries': 3, 'balances': {'MERCHANT': '22.00', 'RIDER': '5.00', 'PLATFORM': '3.00'}}
DUP_ORDER_EVENT {'applied': False, 'reason': 'DUPLICATE_EVENT', 'status': 'DELIVERED'}
REFUND {'applied': True, 'status': 'PARTIAL_REFUNDED', 'entries': 4, 'balances': {'MERCHANT': '18.00', 'RIDER': '5.00', 'PLATFORM': '3.00'}}
DUP_REFUND {'applied': False, 'reason': 'DUPLICATE_EVENT', 'status': 'PARTIAL_REFUNDED'}
BALANCED True
EXPECTED_INVALID PARTIAL_REFUNDED -> DELIVERED not allowed
EXPECTED_SKIP CREATED -> DELIVERED not allowed
异常反例一是未取货直接从 PAID 推进到 DELIVERED,状态机拒绝,且没有产生骑手收益;异常反例二是退款完成后又收到新的送达事件,系统拒绝非法迁移。还有一种常见错误是只按“订单号+事件类型”去重:同一订单确实可能发生多次合法退款,简单的类型去重会误伤第二次售后。更稳妥的做法是每次业务动作生成独立 idempotency_key,由事件表唯一约束兜底,同时保留订单号、金额项、操作角色和时间点。
| 演示输入 | 预期输出 | 选型判断 |
|---|---|---|
| 正常下单、接单、取货、送达 | 状态推进一次,生成商家、骑手、平台三条分录 | 四端订单链路与分账字段可追溯 |
| 同一送达事件重复提交 | applied 为 False,分录数不变,余额不变 | 回调重试不会重复记骑手收益 |
| 未取货直接送达 | 状态迁移被拒绝,无新分录 | 状态机约束可防止跳步 |
| 部分退款后重复退款 | 首次生成反向分录,重复请求不二次冲回 | 退款冲回与商家应结可核对 |
五、部署差异与复现验证
本地示例使用内存字典,生产环境要把 event_store、key_store 和 entries 换成数据库唯一约束与事务。可选部署在支持事务和唯一索引的关系库上,例如 PostgreSQL 方向的服务;如果引入消息队列,重复投递仍然要由消费端幂等表兜底。本地演示与云上数据库在并发锁、事务隔离、连接池和监控告警上存在差异,示例结果只说明核心逻辑,不等同于现网性能或客户实测。
复现步骤:准备 Python 3.10 及以上环境,把第三段代码保存为 sample.py,运行 python sample.py。正常输出应得到 DELIVERED 状态、3 条分录和 22/5/3 的余额;重复送达与重复退款应返回 DUPLICATE_EVENT;非法状态迁移应输出 EXPECTED_INVALID 或 EXPECTED_SKIP。本文给出的是预期输出,实际运行时保存解释器版本、完整日志和代码版本,便于团队复核。生产改造时,在数据库中增加 SELECT ... FOR UPDATE 或等价行锁,并将状态更新、事件插入、分录插入放在同一事务内。
六、把技术结果写进选型验收单
同城外卖系统演示时,可要求服务方按上表现场走单,并回答四个问题:每个外部回调是否要求幂等键;状态迁移是否有服务端约束;退款是否生成独立反向分录;事件、分录和余额能否按订单导出。乡镇场景还应追加弱网重试、跨村地址、超范围地址、兼职骑手转单和分站权限演示。只要重复送达能多记一笔骑手收益,或退款后商家应结无法追溯,系统就还需要继续核对实现方式。
上线前清单可收束为六项:用一笔真实金额订单走通四端;记录每个状态迁移的时间点和操作角色;重复提交送达与退款;核对商家、骑手、平台三方余额;检查分站账号能否看到本区域订单和分录;把版本、接口、费用和售后边界写入项目方案。把这些结果保存成验收记录,再比较不同系统的配置和报价,选型会更有依据。