先回答“哪里买”:校园跑腿系统可按项目阶段、业务规则、技术团队与预算,选择SaaS、私有化部署、源码安装和个性化开发等实施方式,常见采购路径包括按年或按月租用、购买源码并私有化部署、委托定制开发。低预算、技术人手少的单校或两三个校区项目,先从供应商官方渠道申请演示和分项报价;有技术团队、长期多校区或确有接口扩展需求的团队,再评估源码或定制。无论走哪条路径,采购演示都应落到一个可复现的工程故障:多校区、校门中转和楼栋分拣同时存在时,订单状态、配送任务和分账分录是否一致。本文用MySQL 8.0数据模型和SQL复现“重复扫码导致重复分账、退款后账未冲回”的验收场景,作为选型时的技术核对清单。
一、采购前把“哪里买”拆成可验收的技术边界
采购渠道不同,演示深度也应不同。下表把三种交付方式放在同一组验收项下,避免只比较页面和报价。
| 采购方式 | 适用条件 | 向供应商索取 | 技术验收重点 |
|---|---|---|---|
| SaaS租赁 | 预算有限、无服务器运维、希望快速上线 | 演示账号、模块清单、数据导出说明、接口幂等说明 | 重复扫码、退款对账、分校区汇总 |
| 源码或私有化 | 有技术团队、长期多校区、需要深度扩展 | 部署文档、数据库结构、许可与升级条款、备份恢复方案 | 重复回调、数据迁移、分账分录唯一性 |
| 定制开发 | 业务规则差异大、接口多、需要独立流程 | 需求说明书、接口文档、测试用例、验收脚本 | 状态机、幂等键、退款冲回、跨校区对账 |
表格不是推荐某类产品,而是把演示从“看页面”变成“走一笔订单”。采购时要求供应商按本校真实校区、楼栋和交接点配置一个演示,记录订单号、任务号、批次号、分账分录和退款单号,后续每个异常都用同一组编号核对。

微订校园平台校区选择页与骑手宿舍分组送达页。
二、数据模型与字段约束
以下模型不依赖某家产品,可作为验收底稿。核心是让订单、任务、批次、分账和退款各有唯一业务键。
-- MySQL 8.0
CREATE TABLE campus_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
campus_id BIGINT UNSIGNED NOT NULL,
building_id BIGINT UNSIGNED NOT NULL,
status ENUM('CREATED','PAID','ACCEPTED','AT_GATE','SORTED','DELIVERING','DELIVERED','SETTLED') NOT NULL DEFAULT 'CREATED',
pay_amount DECIMAL(10,2) NOT NULL,
refund_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
version INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_no (order_no),
KEY idx_campus_status (campus_id, status),
CONSTRAINT ck_order_amount CHECK (pay_amount >= 0 AND refund_amount >= 0 AND refund_amount <= pay_amount)
) ENGINE=InnoDB;
CREATE TABLE delivery_task (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
task_no VARCHAR(32) NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
task_type ENUM('PICKUP','TRANSFER','DELIVERY') NOT NULL,
status ENUM('WAITING','ACCEPTED','DONE','CANCELED') NOT NULL DEFAULT 'WAITING',
assignee_id BIGINT UNSIGNED NULL,
batch_id BIGINT UNSIGNED NULL,
version INT UNSIGNED NOT NULL DEFAULT 0,
UNIQUE KEY uk_task_no (task_no),
KEY idx_order_id (order_id),
CONSTRAINT fk_task_order FOREIGN KEY (order_id) REFERENCES campus_order(id)
) ENGINE=InnoDB;
CREATE TABLE transfer_batch (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
batch_no VARCHAR(32) NOT NULL,
campus_id BIGINT UNSIGNED NOT NULL,
gate_id BIGINT UNSIGNED NOT NULL,
status ENUM('OPEN','SORTING','DISPATCHED','CLOSED') NOT NULL DEFAULT 'OPEN',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_batch_no (batch_no),
KEY idx_campus_gate_status (campus_id, gate_id, status)
) ENGINE=InnoDB;
CREATE TABLE settlement_entry (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
entry_type ENUM('PLATFORM_FEE','MERCHANT_PAYABLE','RIDER_INCOME') NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status ENUM('PENDING','SETTLED','REVERSED') NOT NULL DEFAULT 'PENDING',
UNIQUE KEY uk_order_entry (order_id, entry_type),
CONSTRAINT fk_settlement_order FOREIGN KEY (order_id) REFERENCES campus_order(id)
) ENGINE=InnoDB;
CREATE TABLE refund_record (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
refund_no VARCHAR(32) NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status ENUM('CREATED','SUCCESS','FAILED') NOT NULL DEFAULT 'CREATED',
UNIQUE KEY uk_refund_no (refund_no),
KEY idx_refund_order (order_id),
CONSTRAINT fk_refund_order FOREIGN KEY (order_id) REFERENCES campus_order(id)
) ENGINE=InnoDB;
关键约束有三处:settlement_entry的uk_order_entry防止同一订单同一种分账被重复写入;refund_record的uk_refund_no防止同一退款单号重复冲账;campus_order.version用于条件更新,避免重复回调把订单状态推进两次。

用户支付经分账规则和区域节点分配至商家、骑手与平台的流程示意。
三、核心事务:批量送达与分账只落一次
下面的事务模板可用于验收。执行顺序是:锁订单行、条件更新订单状态、条件更新配送任务、写入分账分录。每一步都要检查影响行数,而不是只看接口返回成功。
-- 参数:p_order_id, p_task_id
START TRANSACTION;
SELECT status, pay_amount, refund_amount
FROM campus_order
WHERE id = p_order_id
FOR UPDATE;
UPDATE campus_order
SET status = 'DELIVERED', version = version + 1
WHERE id = p_order_id AND status = 'DELIVERING';
-- 预期 ROW_COUNT() = 1,否则 ROLLBACK
UPDATE delivery_task
SET status = 'DONE', version = version + 1
WHERE id = p_task_id AND order_id = p_order_id AND status = 'ACCEPTED';
-- 预期 ROW_COUNT() = 1,否则 ROLLBACK
INSERT INTO settlement_entry (order_id, entry_type, amount, status) VALUES
(p_order_id, 'PLATFORM_FEE', 3.00, 'PENDING'),
(p_order_id, 'MERCHANT_PAYABLE', 36.00, 'PENDING'),
(p_order_id, 'RIDER_INCOME', 6.00, 'PENDING')
ON DUPLICATE KEY UPDATE entry_type = VALUES(entry_type);
COMMIT;
这段事务的核心不是“写得长”,而是让重复请求第二次执行时影响行数为0:订单状态只前进一次,配送任务只完成一次,分账分录不新增。采购演示时,可要求连续点击两次“批量送达”或让回调重试两次,再查看settlement_entry数量和订单版本号。

微订校园已收订单按宿舍分组,可勾选后送达站点或用户。
四、正常输入输出:一笔校门中转订单
输入示例:order_no=O1001,campus_id=1,building_id=3,pay_amount=45.00;平台费3.00、商家应结36.00、骑手收入6.00。按“支付→接单→到站→分拣→送达→结算”执行。预期输出如下,均为演示环境按脚本执行应得到的结果,不代表任何真实平台数据。
| 检查项 | 预期值 |
|---|---|
| campus_order.status | DELIVERED |
| delivery_task.status | DONE |
| settlement_entry 行数 | 3 |
| 三笔分账合计 | 45.00 |
| 重复执行批量送达后的分账行数 | 仍为3 |
再提交一笔部分退款:refund_no=R1001,退款金额5.00。退款事务先写入退款单号,再按订单行锁更新分账金额。拆分示例仅用于演示:商家应结冲回1.67、平台费冲回0.83、骑手收入冲回2.50,退款后三笔合计40.00。
START TRANSACTION;
INSERT INTO refund_record (refund_no, order_id, amount, status)
VALUES ('R1001', 1, 5.00, 'SUCCESS')
ON DUPLICATE KEY UPDATE refund_no = VALUES(refund_no);
-- 若 ROW_COUNT() = 0,说明退款单号重复,不再冲账
UPDATE settlement_entry SET amount = amount - 1.67
WHERE order_id = 1 AND entry_type = 'MERCHANT_PAYABLE' AND amount >= 1.67;
UPDATE settlement_entry SET amount = amount - 0.83
WHERE order_id = 1 AND entry_type = 'PLATFORM_FEE' AND amount >= 0.83;
UPDATE settlement_entry SET amount = amount - 2.50
WHERE order_id = 1 AND entry_type = 'RIDER_INCOME' AND amount >= 2.50;
UPDATE campus_order
SET refund_amount = refund_amount + 5.00
WHERE id = 1 AND refund_amount + 5.00 <= pay_amount;
COMMIT;
执行后应核对:refund_record只有1条,campus_order.refund_amount为5.00,settlement_entry三笔金额合计40.00。若同一退款单号再次提交,退款记录不新增,分账金额不应再次减少。
五、异常反例:重复扫码或重复回调导致重复分账
场景A:骑手在收餐点连扫两次,或送达回调重试,分账逻辑被调用两次。如果分账表没有唯一键,结果会直接翻倍。下面的错误表结构可用于复现:
CREATE TABLE settlement_entry_bad (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
entry_type VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
INSERT INTO settlement_entry_bad (order_id, entry_type, amount) VALUES (1, 'PLATFORM_FEE', 3.00);
INSERT INTO settlement_entry_bad (order_id, entry_type, amount) VALUES (1, 'PLATFORM_FEE', 3.00);
SELECT order_id, entry_type, COUNT(*) AS cnt, SUM(amount) AS total
FROM settlement_entry_bad
GROUP BY order_id, entry_type;
预期异常输出:PLATFORM_FEE的cnt=2、total=6.00;如果商家应结和骑手收入也重复写入,三笔分账合计会超过订单实付。根因不是“扫码设备慢”,而是业务键没有进入数据库约束。修复后重复执行同一事务,第二次插入触发uk_order_entry,ON DUPLICATE KEY不新增行,分录数仍为3。
场景B:退款先成功,但分账更新失败;或同一退款单号被提交两次。若没有refund_no唯一键和条件更新,可能出现退款记录两条、商家应结冲回两次。修复方式是把退款单号作为幂等键,并在事务内先插入退款记录,再更新分账;第二次相同单号插入影响行数为0,不再冲账。验收时用同一退款单号提交两次,预期refund_record只有1条,settlement_entry金额只减少一次。
六、把技术验收写进采购演示和合同
采购时可以让演示按以下顺序留痕,而不是只看“下单成功”:
用真实校区、校门、楼栋配置一个演示订单,记录订单号、任务号、批次号。
连续触发两次批量送达或回调,核对settlement_entry数量不变、订单版本号只加1。
提交一笔部分退款,核对退款单号唯一、三笔分账金额与订单退款金额相等。
切换两个校区查看汇总,核对分站流水与订单明细能对上。
合同或验收单可写明:接口需支持幂等键;分账分录需有唯一约束;退款需有唯一退款单号和冲回规则;演示需提供重复回调、部分退款、跨校区汇总的预期输出。SaaS、源码或定制只是交付方式不同,验收项应一致。这样回答“哪里买”时,读者拿到的不只是供应商名单,而是一套能复现、能对账的技术核对方法。
作者:微订(上海逊柯计算机科技有限公司)。