能快速上线的校园订餐小程序,选型时不要只问系统写多久;先把上线拆成资料、认证、支付、配置、测试、审核六项,再用一笔餐从档口到宿舍做技术验收。真正影响上线后稳定的,是订单状态机、交接明细唯一约束、幂等键和结算防重。下面以校门中转与楼栋分拣中的一个典型故障为例,给出可复现的数据模型与SQL。
一、快速上线要验收的三件事
第一是状态机。一笔校园餐至少应显式区分已支付、商家接单、已取餐、到校门、已分拣、配送中、已送达、待结算、已结算,以及取消、退款、异常挂起。批量中转和批量送达只能从明确的前置状态迁移,不能用一个“完成”按钮跳过中间环节。
第二是幂等。校门收餐常出现重复扫码、网络重试、操作员连续点击。同一交接批次应有唯一幂等键,同批次同一订单只能产生一条交接明细。重复请求应返回第一次结果,而不是再次推进状态、再次发通知、再次生成结算。

微订校园配送按商家汇总订单,并提供扫码收单入口。
第三是对账。送达事件、通知事件、结算任务应分别有防重键。退款冲回时沿用原订单号写入退款类型结算任务,让用户实付、商家应结、骑手收入、平台抽成和退款冲回能对应同一订单。
| 上线环节 | 负责人要交的凭证 | 技术验收点 |
|---|---|---|
| 资料与主体 | 主体、商户、小程序账号清单 | 校区、站点、角色权限可配 |
| 认证与支付 | 认证回执、支付商户号开通记录 | 支付回调与退款回调可重放 |
| 后台配置 | 校区、楼栋、站点、班次配置表 | 地址层级与分拣分组一致 |
| 订单测试 | 正常单、重复扫码、状态不符记录 | 状态迁移与冲突可追踪 |
| 小程序审核 | 提交版本与审核结果 | 审核版本与后台配置一致 |
| 试运营 | 高峰班次与异常处理表 | 通知、结算、退款不重复 |
二、订单状态与约束
示例状态流为:PAID→ACCEPTED→PICKED_UP→AT_GATE→SORTED→OUT_FOR_DELIVERY→DELIVERED→SETTLEMENT_PENDING→SETTLED。取消走CANCELLED,退款完成走REFUNDED,异常挂起走EXCEPTION_HOLD。批量送达只允许把OUT_FOR_DELIVERY推进到DELIVERED,并且同时校验station_id和version。
version用于乐观锁。两个操作员同时扫同一订单时,只有一个更新能成功;另一个得到STALE_VERSION或STATUS_MISMATCH,进入冲突列表,不产生通知和结算。这样现场可以继续处理其他订单,冲突单由专人复核。
三、数据模型与SQL
以下为MySQL 8.0示例。订单表保存当前状态和版本;交接批次表用幂等键防重;交接明细表用联合主键防同一批次重复订单;结算任务表用订单号加业务类型防重复结算;通知发件箱用去重键防重复通知。
CREATE TABLE campus_order (
order_id BIGINT PRIMARY KEY,
campus_id BIGINT NOT NULL,
station_id BIGINT NOT NULL,
building_id BIGINT NOT NULL,
status VARCHAR(24) NOT NULL,
version INT NOT NULL DEFAULT 0,
pay_amount DECIMAL(10,2) NOT NULL,
refund_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
CONSTRAINT ck_order_status CHECK (status IN ('PAID','ACCEPTED','PICKED_UP','AT_GATE','SORTED','OUT_FOR_DELIVERY','DELIVERED','SETTLEMENT_PENDING','SETTLED','CANCELLED','REFUNDED','EXCEPTION_HOLD')),
CONSTRAINT ck_order_amount CHECK (pay_amount >= 0 AND refund_amount >= 0 AND refund_amount <= pay_amount)
);
CREATE TABLE handoff_batch (
handoff_id BIGINT PRIMARY KEY AUTO_INCREMENT,
idempotency_key VARCHAR(96) NOT NULL,
station_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL,
batch_type VARCHAR(16) NOT NULL,
result_json JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_handoff_idem (idempotency_key),
CONSTRAINT ck_handoff_type CHECK (batch_type IN ('TRANSFER','DELIVER'))
);
CREATE TABLE handoff_item (
handoff_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
expected_status VARCHAR(24) NOT NULL,
expected_version INT NOT NULL,
scan_result VARCHAR(24) NOT NULL DEFAULT 'PENDING',
conflict_code VARCHAR(32) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (handoff_id, order_id),
CONSTRAINT fk_item_batch FOREIGN KEY (handoff_id) REFERENCES handoff_batch(handoff_id),
CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES campus_order(order_id),
CONSTRAINT ck_scan_result CHECK (scan_result IN ('PENDING','APPLIED','DUPLICATE','CONFLICT'))
);
CREATE TABLE settlement_task (
order_id BIGINT NOT NULL,
biz_type VARCHAR(16) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
PRIMARY KEY (order_id, biz_type),
CONSTRAINT ck_settle_type CHECK (biz_type IN ('DELIVERY','REFUND')),
CONSTRAINT ck_settle_status CHECK (status IN ('PENDING','DONE','CANCELLED'))
);
CREATE TABLE notify_outbox (
event_id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
event_type VARCHAR(24) NOT NULL,
dedup_key VARCHAR(96) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
UNIQUE KEY uk_notify_dedup (dedup_key)
);
四、核心实现:批量送达存储过程
存储过程先锁定批次幂等键;若result_json已有值,说明该批次处理过,直接返回历史结果。否则写入交接明细,按订单号、站点、前置状态和版本执行更新,只给真正迁移成功的订单写通知与结算任务。下方为完整核心逻辑,示例未连接真实支付、地图或消息通道,输出为按代码逻辑推演的预期结果。
DELIMITER //
CREATE PROCEDURE sp_batch_deliver(
IN p_idem VARCHAR(96),
IN p_station BIGINT,
IN p_operator BIGINT,
IN p_items JSON
)
BEGIN
DECLARE v_handoff BIGINT;
DECLARE v_prev JSON;
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END;
START TRANSACTION;
INSERT INTO handoff_batch(idempotency_key,station_id,operator_id,batch_type,result_json)
VALUES(p_idem,p_station,p_operator,'DELIVER',NULL)
ON DUPLICATE KEY UPDATE handoff_id=LAST_INSERT_ID(handoff_id);
SET v_handoff=LAST_INSERT_ID();
SELECT result_json INTO v_prev
FROM handoff_batch WHERE handoff_id=v_handoff FOR UPDATE;
IF v_prev IS NOT NULL THEN
COMMIT;
SELECT v_prev AS result_json;
ELSE
INSERT IGNORE INTO handoff_item(handoff_id,order_id,expected_status,expected_version)
SELECT v_handoff, jt.order_id, jt.expected_status, jt.expected_version
FROM JSON_TABLE(p_items, '$[*]' COLUMNS(
order_id BIGINT PATH '$.orderId',
expected_status VARCHAR(24) PATH '$.expectedStatus',
expected_version INT PATH '$.expectedVersion'
)) jt;
UPDATE campus_order o
JOIN handoff_item i ON i.order_id=o.order_id AND i.handoff_id=v_handoff
SET o.status='DELIVERED', o.version=o.version+1
WHERE o.station_id=p_station
AND o.status=i.expected_status
AND o.status='OUT_FOR_DELIVERY'
AND o.version=i.expected_version;
UPDATE handoff_item i
JOIN campus_order o ON o.order_id=i.order_id
SET i.scan_result = CASE
WHEN o.status='DELIVERED' AND o.version=i.expected_version+1 THEN 'APPLIED'
ELSE 'CONFLICT' END,
i.conflict_code = CASE
WHEN o.status<>i.expected_status THEN 'STATUS_MISMATCH'
WHEN o.version<>i.expected_version THEN 'STALE_VERSION'
ELSE NULL END
WHERE i.handoff_id=v_handoff;
INSERT IGNORE INTO settlement_task(order_id,biz_type,status)
SELECT i.order_id,'DELIVERY','PENDING'
FROM handoff_item i
WHERE i.handoff_id=v_handoff AND i.scan_result='APPLIED';
INSERT IGNORE INTO notify_outbox(order_id,event_type,dedup_key)
SELECT i.order_id,'DELIVERED',CONCAT('DELIVER:',i.order_id)
FROM handoff_item i
WHERE i.handoff_id=v_handoff AND i.scan_result='APPLIED';
UPDATE handoff_batch b
SET b.result_json = (
SELECT JSON_ARRAYAGG(JSON_OBJECT('orderId',i.order_id,'result',i.scan_result,'conflict',i.conflict_code))
FROM handoff_item i WHERE i.handoff_id=v_handoff
) WHERE b.handoff_id=v_handoff;
COMMIT;
SELECT result_json FROM handoff_batch WHERE handoff_id=v_handoff;
END IF;
END//
DELIMITER ;
五、正常输入输出
先插入三笔测试订单:1001和1002为OUT_FOR_DELIVERY,1003为AT_GATE,1004为OUT_FOR_DELIVERY但版本故意设为9。调用正常批次1001、1002,期望两笔都迁移成功,生成两条通知和两条结算任务。
INSERT INTO campus_order(order_id,campus_id,station_id,building_id,status,version,pay_amount)
VALUES
(1001,1,101,8,'OUT_FOR_DELIVERY',4,25.00),
(1002,1,101,9,'OUT_FOR_DELIVERY',6,18.00),
(1003,1,101,9,'AT_GATE',2,20.00),
(1004,1,101,8,'OUT_FOR_DELIVERY',9,22.00);
CALL sp_batch_deliver('DELIVER:S01:B08:0001',101,9001,
JSON_ARRAY(
JSON_OBJECT('orderId',1001,'expectedStatus','OUT_FOR_DELIVERY','expectedVersion',4),
JSON_OBJECT('orderId',1002,'expectedStatus','OUT_FOR_DELIVERY','expectedVersion',6)
));
预期返回按orderId查看为:1001 APPLIED、1002 APPLIED;订单状态均为DELIVERED,版本分别为5和7;settlement_task新增两条DELIVERY,notify_outbox新增两条DELIVERED。用同一幂等键再次调用,返回同一结果,订单版本、结算任务数和通知数不再增加。
六、异常反例
异常批次同时传入1003和1004。1003实际状态为AT_GATE,与期望OUT_FOR_DELIVERY不符;1004状态相同但版本为9,与期望版本8不符。预期只产生冲突记录,不改变订单,不写通知,不写结算任务。
CALL sp_batch_deliver('DELIVER:S01:B08:0002',101,9001,
JSON_ARRAY(
JSON_OBJECT('orderId',1003,'expectedStatus','OUT_FOR_DELIVERY','expectedVersion',2),
JSON_OBJECT('orderId',1004,'expectedStatus','OUT_FOR_DELIVERY','expectedVersion',8)
));
预期返回:1003为CONFLICT,conflict_code为STATUS_MISMATCH;1004为CONFLICT,conflict_code为STALE_VERSION。1003仍为AT_GATE,1004仍为OUT_FOR_DELIVERY且版本为9;两笔均无新增结算任务和通知。若同一批次重复传入同一orderId,handoff_item联合主键只保留一条明细,结算与通知只生成一次。
七、业务验收清单
演示时不要只看页面能否点按钮。用本校真实楼栋和站点,要求对方连续做四件事:同一幂等键重放一次;同一订单重复扫码一次;传入一笔状态不符订单;两个操作员并发操作同一订单。观察是否返回历史结果、是否出现冲突列表、是否误发通知、是否重复生成结算或退款冲回。

微订校园配送站点收单与已收订单送达页面,按宿舍组织处理。
| 验收项 | 通过标准 | 异常信号 |
|---|---|---|
| 重复扫码 | 交接明细只有一条,状态只推进一次 | 同一单出现多条送达记录 |
| 同键重试 | 返回首次结果,通知和结算不增加 | 重试后账单笔数增加 |
| 状态不符 | 进入冲突列表,原订单不变 | 批量操作把未交接单标记为已送达 |
| 并发操作 | 只有一方成功,另一方有冲突码 | 双方都提示成功 |
| 退款冲回 | 按原订单号生成REFUND任务,不重复 | 退款与结算对不上同一单 |
把上线拆成资料、认证、支付、配置、测试、审核,再把上述状态机、唯一约束、幂等键和结算防重列入演示脚本,快速上线才有可核对的工程依据。小程序审核和支付开通时间以对应平台为准,代码示例中的输出为逻辑推演,不替代真实环境联调。
作者:微订(上海逊柯计算机科技有限公司)。