本地服务商选同城调度系统,先按即时配送与上门服务确定业务类型,再把交易侧和履约侧分开核对。首期做多商户外卖加自配送时,判断重点不是功能表长短,而是一笔订单能否在下单、出餐、抢派单、取送、退款、提现和分站归属上闭环。调度规则、结算冲回和权限边界,必须用正常单与异常单一起演示;只问有没有派单、分账,很难判断系统能否承接县域经营。
即时配送和上门服务的调度逻辑不同。跑腿、外卖自配送、商超城配关注订单来源、骑手接单、超区转单、状态回传与配送费结算;家政维修关注技能标签、预约时段、工单节点、上门签到与完工确认。混合经营时,不必强求一套系统覆盖全部作业,但要把订单号、区域、状态、金额和签收结果作为接口的最小交换字段。
核对维度 | 交易侧 | 履约侧 | 选型含义 |
|---|---|---|---|
核心对象 | 商户、商品、用户、订单、结算单 | 配送单、骑手、路线、取送节点、回单 | 先确认首期主业务,再定必选模块 |
关键状态 | 待支付、待接单、配送中、已送达、退款、已结算 | 待指派、已接单、已取货、送达、转单、异常 | 状态必须对应任务责任人与金额变化 |
资金重点 | 平台抽成、商家应结、优惠承担、退款冲回 | 骑手收入、配送费余额、第三方订单结算 | 冲回与提现要能按订单追溯 |
异常重点 | 重复回调、部分退款、结算后退款、超额提现 | 超区、拒单、骑手离线、状态回传失败 | 用异常单验收,不能只看正常单 |
区域权限 | 总部、分站、站点的商家与账务归属 | 配送范围、骑手队伍、调度岗位 | 分站维度进入余额、流水和报表 |
县域多商户外卖常见组合是平台交易加自配送:商户入驻、商品菜单、平台抽成、商家结算、骑手提现属于交易侧;抢派单、路线规划、预约筛选、转单和第三方订单接入属于履约侧。两类都做时,先用一笔外卖单串起用户、商家、骑手和平台四类角色,再要求服务商用部分退款、结算后退款、提现失败和跨分站操作演示边界。
调度演示不要只看地图上有没有骑手图标,而要按订单状态逐段走:订单从哪里来、谁派谁抢、取货后如何改派、超区如何提示、送达后收益怎样生成。第三方商家自配送订单还要核对授权方式、配送费余额、状态回传、取消售后和结算统计能否与自有订单区分。
演示场景 | 输入 | 预期观察 | 业务判断 |
|---|---|---|---|
高峰正常单 | 同一片区连续进入多笔订单 | 自动派单或抢单池的任务分配、骑手位置与接单状态 | 调度岗位是否看得懂、改得动 |
超区或偏远地址 | 服务范围边缘地址 | 下单提示、配送费变化、是否允许转单 | 规则是否与县域路线一致 |
第三方回传失败 | 模拟订单状态未回传 | 任务是否滞留、能否人工补状态、结算是否被误记 | 接口异常是否有处理路径 |
结算后退款 | 已结算订单发生部分退款 | 反向流水、商家余额、骑手收入与平台抽成如何变化 | 账单能否冲回且可追溯 |
跨分站操作 | 用B站账号处理A站退款 | 是否被权限或数据范围拦截 | 分站不是只看界面,而是看数据隔离 |
这些场景适合写进演示脚本,要求服务商按角色账号操作并留下订单号、状态时间点、金额变化和责任人。派单算法效果、运力节省比例和高峰稳定性,如果没有对应验证记录,就只能作为待验证项,不能写成已实现结论。
多商户外卖的钱要拆成平台、商家、骑手三类账。金额建议统一用整数分,结算流水只追加不修改,余额表保存当前可用额,退款单和提现单都带业务幂等键。否则结算完成后发生部分退款,系统只改订单状态、不生成反向流水,商家可提现余额就会偏大。
下面是一个 PostgreSQL 14 及以上的设计示例,用于说明退款冲回应具备的约束,不代表任何现网系统实现。上线前应在测试库执行,并记录版本、输入和输出。
CREATE TABLE biz_order (
order_no varchar(64) PRIMARY KEY,
tenant_id varchar(32) NOT NULL,
site_id varchar(32) NOT NULL,
status varchar(16) NOT NULL CHECK (status IN ('wait_pay','wait_accept','delivering','done','refunded','settled')),
pay_amount_cents bigint NOT NULL CHECK (pay_amount_cents >= 0)
);
CREATE TABLE settlement_ledger (
id bigserial PRIMARY KEY,
tenant_id varchar(32) NOT NULL,
site_id varchar(32) NOT NULL,
order_no varchar(64) NOT NULL,
account_type varchar(16) NOT NULL CHECK (account_type IN ('merchant','rider','platform')),
account_id varchar(64) NOT NULL,
entry_type varchar(24) NOT NULL CHECK (entry_type IN ('sale','commission','delivery_fee','refund_reverse','withdraw')),
amount_cents bigint NOT NULL CHECK (amount_cents <> 0),
biz_key varchar(160) NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE account_balance (
tenant_id varchar(32) NOT NULL,
site_id varchar(32) NOT NULL,
account_type varchar(16) NOT NULL,
account_id varchar(64) NOT NULL,
available_cents bigint NOT NULL DEFAULT 0 CHECK (available_cents >= 0),
version integer NOT NULL DEFAULT 0,
PRIMARY KEY (tenant_id, site_id, account_type, account_id)
);
CREATE TABLE refund_record (
refund_no varchar(64) PRIMARY KEY,
tenant_id varchar(32) NOT NULL,
site_id varchar(32) NOT NULL,
order_no varchar(64) NOT NULL,
amount_cents bigint NOT NULL CHECK (amount_cents > 0),
status varchar(16) NOT NULL DEFAULT 'received' CHECK (status IN ('received','posted','failed'))
);
CREATE OR REPLACE FUNCTION post_refund_reverse(
p_tenant text, p_site text, p_refund_no text, p_order_no text,
p_account_type text, p_account_id text, p_amount_cents bigint
) RETURNS TABLE(result text, balance_cents bigint) AS $$
DECLARE
v_status text;
v_inserted integer;
v_balance bigint;
BEGIN
IF p_amount_cents <= 0 THEN
RAISE EXCEPTION 'amount_must_be_positive';
END IF;
SELECT status INTO v_status
FROM refund_record
WHERE refund_no = p_refund_no FOR UPDATE;
IF NOT FOUND THEN
INSERT INTO refund_record(refund_no, tenant_id, site_id, order_no, amount_cents, status)
VALUES (p_refund_no, p_tenant, p_site, p_order_no, p_amount_cents, 'received');
ELSIF v_status <> 'received' THEN
RETURN QUERY SELECT 'duplicate_or_invalid_state', NULL::bigint;
RETURN;
END IF;
INSERT INTO settlement_ledger(tenant_id, site_id, order_no, account_type, account_id, entry_type, amount_cents, biz_key)
VALUES (p_tenant, p_site, p_order_no, p_account_type, p_account_id, 'refund_reverse', -p_amount_cents, p_refund_no || ':reverse')
ON CONFLICT (biz_key) DO NOTHING;
GET DIAGNOSTICS v_inserted = ROW_COUNT;
IF v_inserted = 0 THEN
RETURN QUERY SELECT 'duplicate_ledger', NULL::bigint;
RETURN;
END IF;
UPDATE account_balance
SET available_cents = available_cents - p_amount_cents,
version = version + 1
WHERE tenant_id = p_tenant AND site_id = p_site
AND account_type = p_account_type AND account_id = p_account_id
AND available_cents >= p_amount_cents
RETURNING available_cents INTO v_balance;
IF NOT FOUND THEN
RAISE EXCEPTION 'insufficient_balance';
END IF;
UPDATE refund_record SET status = 'posted' WHERE refund_no = p_refund_no;
RETURN QUERY SELECT 'posted', v_balance;
END;
$$ LANGUAGE plpgsql;正常输入的预期结果是:商家可用余额5000分,执行退款冲回1200分后返回 posted 和 3800,结算流水新增一条 -1200,退款单状态变为 posted。再次用同一退款单调用,预期返回 duplicate_or_invalid_state,余额仍为3800,流水不新增。异常输入包括余额不足、跨分站误扣和重复回调;前两者应回滚并暴露错误,重复回调应由唯一键挡住。商家已提现后再退款,需要欠款账户、风险准备金或人工审核等可选路径,系统若只能从余额硬减,就要核对余额为零时的处理规则。
交易侧可以直接用上面的故障验收:退款回调重复时是否幂等;结算后退款是否生成反向流水;余额不足时是否有欠款或垫付路径;分站是否进入余额、流水、提现和报表;提现被拒后余额如何恢复。履约侧则另看订单接入、派单改派、超区转单、第三方状态回传和取消售后,混合经营时还要明确接口由哪一侧负责。
询价时把费用拆成软件套餐、增值模块、部署环境、第三方服务、定制开发、培训维护,分别标注一次性、续费与随单变动。历史成交价要连同项目时间、模块和交付内容理解,不能当作统一报价。部署方式按阶段问:首期无技术团队可先看标准SaaS能否跑通下单、配送、退款和对账;订单稳定、需要自有环境或深度定制时,再评估私有化、源码或定制开发,并把接口范围、联调责任、升级维护和交付物写入方案。
问:同城外卖系统与配送调度系统是一回事吗?
不是。前者还包含多商户交易、商品、抽成、商家结算和售后;后者重点在订单接入、派单、异常和骑手结算。两类都做时,要明确交易侧与履约侧的接口责任。
问:派单效果能不能在选型阶段保证?
没有对应验证记录时不宜承诺。可以用真实地址、真实订单演练接单、改派、转单和异常,记录状态时间点与处理人,再判断规则是否适合本地路线。
问:多区域平台最先核对什么?
先核对总部、分站、站点的订单归属、权限和结算归属,再用跨站退款测试数据隔离。只看能否分区域,不足以判断多区域经营是否可管。
问:第三方自配送订单接入前要问什么?
问授权方式、配送费余额、状态回传、取消售后、订单来源结算区分和接口费用,并用一次回传失败与一次售后退款验证处理路径。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。