直接答案:同城生活服务小程序不宜先堆四端功能,而应先落一套可核对的交易模型:用户、商家、骑手、平台围绕订单、配送任务、结算单协作;外卖订单带商家,跑腿订单不带商家,但可共用地址、区域和骑手池。工程上优先解决一个高频故障——批量送达或重复扫码时,同一订单被多次结算。下面用 PostgreSQL 16 演示状态校验、幂等事件和结算唯一约束,并给出正常输出、异常反例与校园场景验收动作。

微订校园外卖手机平板与统计后台组合。
一、模型:四端、两条订单流与三类账
四端只是操作入口,真正需要稳定的是订单状态、任务归属和金额归属。外卖订单主链路是“支付—商家接单—出餐—骑手取餐—校门到达—分拣—送达”,跑腿订单是“支付—接单—取件—送达”;两者可共用骑手池,但商家结算只对外卖订单存在,跑腿订单只产生骑手收益与平台服务费。

微订配送调度台,集中显示待指派订单、配送员状态与指派操作。
| 实体 | 关键字段 | 约束重点 |
|---|---|---|
| 订单 | 订单号、类型、校区、站点、楼栋、状态、支付状态、版本号、批次号 | 外卖必须有商家;跑腿商家为空;状态只能按允许路径推进 |
| 配送任务 | 任务号、订单号、骑手、交接站点、交接时间、凭证 | 一个订单同一时刻只对应一个有效任务 |
| 结算流水 | 订单号、收款方、结算类型、金额、来源事件号 | 同一订单、同一收款方、同一事件只允许一条有效结算 |
下面给出可执行的核心表结构。金额计划表用于拆分“平台、商家、骑手”三类账,实际项目应按合同与计费规则写入,不在代码里硬编码抽成比例。
CREATE TABLE biz_order (
order_no varchar(32) PRIMARY KEY,
order_type varchar(16) NOT NULL CHECK (order_type IN ('TAKEOUT','ERRAND')),
scene_type varchar(16) NOT NULL CHECK (scene_type IN ('CAMPUS','CITY')),
merchant_id varchar(32),
campus_id varchar(32),
site_id varchar(32),
building_id varchar(32),
status varchar(24) NOT NULL CHECK (status IN (
'CREATED','PAID','ACCEPTED','COOKED','RIDER_ASSIGNED',
'PICKED_UP','GATE_ARRIVED','SORTED','DELIVERING','DELIVERED',
'CANCELLED','REFUNDING','CLOSED','EXCEPTION')),
payment_status varchar(16) NOT NULL CHECK (
payment_status IN ('UNPAID','PAID','PART_REFUNDED','REFUNDED')),
total_amount numeric(12,2) NOT NULL CHECK (total_amount >= 0),
version_no integer NOT NULL DEFAULT 0,
batch_id varchar(32),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT ck_takeout_merchant CHECK (
(order_type = 'TAKEOUT' AND merchant_id IS NOT NULL)
OR (order_type = 'ERRAND' AND merchant_id IS NULL))
);
CREATE TABLE delivery_task (
task_no varchar(32) PRIMARY KEY,
order_no varchar(32) NOT NULL REFERENCES biz_order(order_no),
rider_id varchar(32),
task_status varchar(16) NOT NULL CHECK (
task_status IN ('WAITING','ACCEPTED','PICKED_UP','HANDOVER','DELIVERED','EXCEPTION')),
handover_site_id varchar(32),
handover_at timestamptz,
proof_url varchar(255),
CONSTRAINT uk_task_order UNIQUE (order_no)
);
CREATE TABLE order_amount_plan (
order_no varchar(32) PRIMARY KEY REFERENCES biz_order(order_no),
merchant_amount numeric(12,2) NOT NULL DEFAULT 0 CHECK (merchant_amount >= 0),
rider_amount numeric(12,2) NOT NULL DEFAULT 0 CHECK (rider_amount >= 0),
platform_amount numeric(12,2) NOT NULL DEFAULT 0 CHECK (platform_amount >= 0)
);
CREATE TABLE order_operation_log (
event_id uuid PRIMARY KEY,
order_no varchar(32) NOT NULL REFERENCES biz_order(order_no),
op_type varchar(24) NOT NULL,
from_status varchar(24) NOT NULL,
to_status varchar(24) NOT NULL,
operator varchar(64) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE settlement_flow (
settle_no bigserial PRIMARY KEY,
order_no varchar(32) NOT NULL REFERENCES biz_order(order_no),
payee_type varchar(16) NOT NULL CHECK (payee_type IN ('MERCHANT','RIDER','PLATFORM')),
settle_type varchar(24) NOT NULL CHECK (
settle_type IN ('ORDER_INCOME','PLATFORM_FEE','REFUND_REVERSAL')),
amount numeric(12,2) NOT NULL,
source_event_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uk_settlement_event UNIQUE (order_no, payee_type, settle_type, source_event_id)
);
CREATE INDEX idx_order_batch_status ON biz_order(batch_id, status);
CREATE INDEX idx_order_campus_site ON biz_order(campus_id, site_id, status);
二、核心实现:一次送达只产生一次结算
批量送达和重复扫码容易出错,根因是前端可能提交整批订单,而服务端逐单更新时没有同时约束“事件幂等”和“状态版本”。下面的函数按四步处理:锁定订单行、校验支付与版本、写入唯一事件号、按金额计划生成结算流水。任一环节失败即整体回滚,重复事件直接返回已处理,不再插入结算。

微订骑手任务与校园宿舍分组双屏。
CREATE OR REPLACE FUNCTION fn_mark_delivered(
p_order_no varchar,
p_expected_version integer,
p_event_id uuid,
p_operator varchar
) RETURNS varchar
LANGUAGE plpgsql AS {mathJaxContainer[0]};
三、复现步骤、正常输出与异常反例
依赖与复现:本地安装 Docker 与 psql,启动 PostgreSQL 16 容器,把上述建表语句保存为 schema.sql 后执行 psql -f schema.sql,再运行下面的样例数据与函数调用。以下为按脚本推演的预期输出,未冒称生产环境或真实云资源执行结果;上线前应在本地或测试环境复现并保存环境版本与完整输出。
INSERT INTO biz_order(order_no, order_type, scene_type, merchant_id, campus_id, site_id, building_id, status, payment_status, total_amount, version_no, batch_id)
VALUES
('ord-tk-0001','TAKEOUT','CAMPUS','mer-hd-01','campus-hd','site-gate-a','bldg-12','SORTED','PAID',32.00,1,'batch-0901'),
('ord-tk-0002','TAKEOUT','CAMPUS','mer-hd-02','campus-hd','site-gate-a','bldg-12','SORTED','UNPAID',18.00,0,'batch-0901'),
('ord-er-0001','ERRAND','CAMPUS',NULL,'campus-hd','site-gate-a','bldg-12','DELIVERING','PAID',12.00,1,NULL);
INSERT INTO order_amount_plan(order_no, merchant_amount, rider_amount, platform_amount)
VALUES
('ord-tk-0001',24.00,5.00,3.00),
('ord-tk-0002',14.00,3.00,1.00),
('ord-er-0001',0.00,9.00,3.00);
SELECT fn_mark_delivered(
'ord-tk-0001', 1,
'11111111-1111-1111-1111-111111111111',
'rider-01'
);
-- 正常输出
fn_mark_delivered
-----------------
DELIVERED
-- 同一事件重复调用
fn_mark_delivered
-----------------
ALREADY_PROCESSED
-- 未支付订单异常
ERROR: ORDER_NOT_PAID:ord-tk-0002
-- 版本号过期异常
ERROR: VERSION_CONFLICT:ord-tk-0001
-- 跑腿订单误填商家金额异常
ERROR: ERRAND_MERCHANT_AMOUNT_INVALID:ord-er-0001
批量操作不宜把整批订单压成一个“全成功或全失败”的事务。更稳妥的做法是先预检,再逐单调用函数并记录每单结果。同一批次中一笔已支付、一笔未支付时,预检应分别给出 READY 与 REJECT,应用层只把 READY 的订单送入送达函数,REJECT 的订单进入异常台账并等待补缴或取消。
SELECT order_no,
status,
payment_status,
version_no,
CASE WHEN status IN ('SORTED','DELIVERING','GATE_ARRIVED')
AND payment_status = 'PAID'
THEN 'READY' ELSE 'REJECT' END AS precheck
FROM biz_order
WHERE batch_id = 'batch-0901'
ORDER BY order_no;
-- 预期输出
-- ord-tk-0001 | SORTED | PAID | 1 | READY
-- ord-tk-0002 | SORTED | UNPAID | 0 | REJECT
四、对账、校园验收与云环境差异
送达之后还要能对账。下面两条查询分别检查“已送达订单的结算合计是否等于订单金额”和“是否存在同一订单、同一收款方、同一结算类型的重复记录”。正常数据下两条查询都应返回空结果;一旦返回记录,应先冻结相关订单的提现或结算批次,再按来源事件号追查。
SELECT o.order_no,
o.total_amount,
COALESCE(SUM(s.amount), 0) AS settled_amount
FROM biz_order o
LEFT JOIN settlement_flow s ON s.order_no = o.order_no
WHERE o.status = 'DELIVERED'
GROUP BY o.order_no, o.total_amount
HAVING o.total_amount <> COALESCE(SUM(s.amount), 0);
SELECT order_no, payee_type, settle_type, count(*) AS cnt
FROM settlement_flow
GROUP BY order_no, payee_type, settle_type
HAVING count(*) > 1;
校园场景的验收动作可以落到一张异常单上:正常订单走商家出餐、校门交接、楼栋分拣、送达;异常订单覆盖未支付、重复扫码、批量送达中部分失败、退款冲回和多校区账号越权。每次记录订单号、当前负责人、状态、金额与来源事件号,客服与财务才能接续处理。校门通行、交接空间、扫码设备与码制属于现场实施条件,应在演示与合同中逐项确认。
| 验收场景 | 输入动作 | 期望结果 |
|---|---|---|
| 正常外卖 | 已支付、已分拣订单调用送达 | 订单变为 DELIVERED,生成商家、骑手、平台三类流水 |
| 正常跑腿 | 无商家订单调用送达 | 只生成骑手与平台流水,不生成商家流水 |
| 重复扫码 | 同一事件号重复提交 | 返回 ALREADY_PROCESSED,不新增结算 |
| 批量部分失败 | 同批次一笔未支付、一笔已支付 | 已支付订单成功,未支付订单进入异常台账,不中断整批 |
| 退款冲回 | 以新事件号写入 REFUND_REVERSAL | 原订单可追溯,冲回金额与退款单关联 |
| 多校区权限 | 校区账号查看他校区订单 | 查询范围受账号权限限制,导出字段与岗位匹配 |
本地容器演示的是事务、行锁与唯一约束行为;生产环境若选用云数据库,需额外核对连接池、网络延迟、备份恢复、审计日志、参数模板和高可用切换。消息队列与缓存只在确有异步通知、削峰或幂等键管理职责时引入,本地演示不等同生产部署。验收以订单、任务、结算三类数据能否互相对上为准,不以页面“看起来跑通”作为唯一标准。
作者:微订(上海逊柯计算机科技有限公司)。