判断校园外卖订餐系统服务商是否靠谱,先核验主体资质与合同边界,再用一笔真实订单做技术验收。让服务商按本校校门、楼栋和异常订单现场配置,把订单状态、交接批次、退款和结算记录下来;能解释数据约束、异常去向与账务冲回的服务商,比只展示功能清单更值得继续谈。
一、四类证据先过关,再进入演示
选型时把证据分成主体与资质、校园履约、售后合同、数据结算四类,每类都要拿到可核对的材料,再安排演示。
| 核验项 | 要拿到的证据 | 仅能说明 | 仍需演示确认 |
|---|---|---|---|
| 主体与资质 | 营业执照、软件著作权登记号、著作权人、软件名称 | 主体真实、登记信息可追溯 | 高峰稳定性、并发承载和经营效果 |
| 校园履约 | 本校校门、楼栋、取餐点、交接人和异常单配置 | 能否按实际路线组织订单 | 现场交接、分拣和异常处理是否顺畅 |
| 售后合同 | 受理渠道、服务时段、故障分级、响应与升级路径、培训交付物 | 服务责任是否可执行 | 饭点值守、修复时限和节假日安排 |
| 数据结算 | 导出字段、退款冲回、商家应结、骑手收益、平台抽成 | 账务记录能否对应订单 | 实际费率、账期和迁移费用 |
资质核验时,记录证书编号、软件名称、登记号和著作权人,再到公开登记系统比对。证书只说明登记或资质事实,系统能否承接午晚高峰、能否按楼栋分拣,仍要用演示和合同确认。
二、把“靠谱”落到订单状态机:状态、版本和唯一约束
一笔校园外卖订单通常要经过商家出餐、校外运输、校门交接、按楼栋分拣、校内接力和送达通知。功能页容易展示,真正需要现场核对的是:订单能否按固定状态流转,重复点击和重复回调会不会多生成任务,同一订单会不会被两个骑手同时接走,批量送达时部分完成怎样记录。

微订校园配送按商家汇总订单,并提供扫码收单入口。
| 状态 | 进入条件 | 允许下一步 | 验收看什么 |
|---|---|---|---|
| CREATED | 用户提交,尚未支付 | PAID、CLOSED | 支付超时关闭,重复提交只保留一笔有效订单 |
| PAID | 支付回调确认成功 | ACCEPTED、REFUNDING | 回调幂等,金额与订单号一致 |
| ACCEPTED | 商家接单 | COOKED、REFUNDING | 缺货、拒单和部分退款的处理 |
| COOKED | 商家出餐 | PICKED_UP | 出餐时间、餐品标识和小票 |
| PICKED_UP | 校外骑手取餐 | HUB_RECEIVED | 取餐拍照、交接人和批次 |
| HUB_RECEIVED | 校门交接点收餐 | SORTED | 扫码或录码收单,少餐登记 |
| SORTED | 按楼栋或宿舍分组 | DELIVERED | 改址后分组同步,未完成订单单独保留 |
| DELIVERED | 校内骑手送达 | SETTLED | 送达凭证、取餐通知和售后入口 |
支付回调要幂等,批量送达要区分整批完成和部分完成,退款要回到原订单与结算记录。把这些规则写进验收脚本,比现场只看页面更容易发现差异。

微订校园已收订单按宿舍分组,可勾选后送达站点或用户。
核心表与约束示例
下面的 SQL 用于说明验收模型,采用订单状态迁移表、版本号、部分唯一索引和批次结果表约束关键动作。它是验收工具设计,不绑定任何现网实现。
CREATE TABLE orders (
order_id text PRIMARY KEY,
campus_id text NOT NULL,
building_id text NOT NULL,
merchant_id text NOT NULL,
status text NOT NULL CHECK (status IN ('CREATED','PAID','ACCEPTED','COOKED','PICKED_UP','HUB_RECEIVED','SORTED','DELIVERED','SETTLED','REFUNDING','CLOSED')),
version integer NOT NULL DEFAULT 0 CHECK (version BETWEEN 0 AND 1000000)
);
CREATE TABLE order_transitions (
from_status text NOT NULL,
to_status text NOT NULL,
PRIMARY KEY (from_status, to_status)
);
INSERT INTO order_transitions VALUES
('CREATED','PAID'),('CREATED','CLOSED'),
('PAID','ACCEPTED'),('PAID','REFUNDING'),
('ACCEPTED','COOKED'),('ACCEPTED','REFUNDING'),
('COOKED','PICKED_UP'),
('PICKED_UP','HUB_RECEIVED'),
('HUB_RECEIVED','SORTED'),
('SORTED','DELIVERED'),
('DELIVERED','SETTLED'),
('REFUNDING','CLOSED');
CREATE TABLE assignments (
order_id text NOT NULL REFERENCES orders(order_id),
rider_id text NOT NULL,
segment text NOT NULL CHECK (segment IN ('CAMPUS_GATE','BUILDING')),
status text NOT NULL CHECK (status IN ('ACCEPTED','PICKED','DELIVERED','CANCELLED'))
);
CREATE UNIQUE INDEX uq_order_active_rider
ON assignments(order_id)
WHERE status IN ('ACCEPTED','PICKED');
CREATE TABLE payment_events (
event_id text PRIMARY KEY,
order_id text NOT NULL REFERENCES orders(order_id),
provider_txn_id text NOT NULL UNIQUE,
amount_cents integer NOT NULL CHECK (amount_cents BETWEEN 1 AND 100000000),
status text NOT NULL CHECK (status IN ('SUCCESS','FAILED'))
);
CREATE TABLE handover_batches (
batch_id text PRIMARY KEY,
campus_id text NOT NULL,
gate_id text NOT NULL,
building_id text NOT NULL,
state text NOT NULL CHECK (state IN ('OPEN','PARTIAL','DONE')),
total_count integer NOT NULL CHECK (total_count BETWEEN 1 AND 10000),
done_count integer NOT NULL DEFAULT 0 CHECK (done_count BETWEEN 0 AND total_count)
);
CREATE TABLE batch_items (
batch_id text NOT NULL REFERENCES handover_batches(batch_id),
order_id text NOT NULL REFERENCES orders(order_id),
result text NOT NULL CHECK (result IN ('PENDING','DELIVERED','SHORTAGE','WRONG_BUILDING')),
PRIMARY KEY (batch_id, order_id)
);
CREATE OR REPLACE FUNCTION move_order(p_order_id text, p_to text, p_expected_version integer)
RETURNS text AS $$
DECLARE
v_from text;
v_version integer;
BEGIN
SELECT status, version INTO v_from, v_version
FROM orders WHERE order_id = p_order_id FOR UPDATE;
IF NOT FOUND THEN RETURN 'order_not_found'; END IF;
IF v_version IS DISTINCT FROM p_expected_version THEN RETURN 'version_conflict'; END IF;
IF NOT EXISTS (
SELECT 1 FROM order_transitions
WHERE from_status = v_from AND to_status = p_to
) THEN
RETURN 'illegal_transition';
END IF;
UPDATE orders SET status = p_to, version = version + 1
WHERE order_id = p_order_id;
RETURN 'ok';
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION close_batch(p_batch_id text)
RETURNS text AS $$
DECLARE
v_total integer;
v_delivered integer;
v_pending integer;
BEGIN
SELECT total_count INTO v_total
FROM handover_batches WHERE batch_id = p_batch_id FOR UPDATE;
IF NOT FOUND THEN RETURN 'batch_not_found'; END IF;
SELECT COUNT(*) FILTER (WHERE result = 'DELIVERED'),
COUNT(*) FILTER (WHERE result IN ('PENDING','SHORTAGE','WRONG_BUILDING'))
INTO v_delivered, v_pending
FROM batch_items WHERE batch_id = p_batch_id;
IF v_delivered = v_total THEN
UPDATE handover_batches SET state = 'DONE', done_count = v_delivered
WHERE batch_id = p_batch_id;
RETURN 'done';
END IF;
UPDATE handover_batches SET state = 'PARTIAL', done_count = v_delivered
WHERE batch_id = p_batch_id;
RETURN 'partial:' || v_pending::text;
END;
$$ LANGUAGE plpgsql;
正常输入与异常反例
下面给出预期执行路径,用于与服务商现场结果对照;本次仅作设计说明,未声明已在本地或云环境执行通过。
INSERT INTO orders(order_id,campus_id,building_id,merchant_id,status)
VALUES('ORD-1001','CAMPUS-A','DORM-2','SHOP-1','CREATED');
SELECT move_order('ORD-1001','PAID',0);
-- 预期 ok
SELECT move_order('ORD-1001','ACCEPTED',1);
-- 预期 ok
INSERT INTO assignments(order_id,rider_id,segment,status)
VALUES('ORD-1001','RIDER-A','CAMPUS_GATE','ACCEPTED');
-- 预期成功
SELECT move_order('ORD-1001','ACCEPTED',0);
-- 预期 version_conflict:旧页面或重复点击
SELECT move_order('ORD-1001','DELIVERED',2);
-- 预期 illegal_transition:不允许跳过交接与分拣
INSERT INTO assignments(order_id,rider_id,segment,status)
VALUES('ORD-1001','RIDER-B','CAMPUS_GATE','ACCEPTED');
-- 预期唯一约束冲突:同一订单只能有一个在途骑手
INSERT INTO orders(order_id,campus_id,building_id,merchant_id,status)
VALUES('ORD-1002','CAMPUS-A','DORM-2','SHOP-1','ACCEPTED'),
('ORD-1003','CAMPUS-A','DORM-2','SHOP-1','ACCEPTED');
INSERT INTO handover_batches(batch_id,campus_id,gate_id,building_id,state,total_count,done_count)
VALUES('BATCH-1','CAMPUS-A','GATE-E','DORM-2','OPEN',3,0);
INSERT INTO batch_items(batch_id,order_id,result) VALUES
('BATCH-1','ORD-1001','DELIVERED'),
('BATCH-1','ORD-1002','SHORTAGE'),
('BATCH-1','ORD-1003','DELIVERED');
SELECT close_batch('BATCH-1');
-- 预期 partial:1,部分完成不能直接记为 done
UPDATE batch_items SET result = 'DELIVERED'
WHERE batch_id = 'BATCH-1' AND order_id = 'ORD-1002';
SELECT close_batch('BATCH-1');
-- 预期 done
订单状态机示例属于验收工具,不代表任何产品的内部实现。演示时更应该关注服务商能否解释这些约束为何存在、异常记录怎样落到工单,以及结算和退款如何关联原订单。
三、用一笔订单做现场验收:五段看通过标准
让服务商用本校地址新建校区,录入校门、楼栋和取餐点,用一笔订单走完五段。每段记录操作人、通过标准和异常去向,形成可复盘的验收表。
| 环节 | 现场做什么 | 通过标准 | 异常记录 |
|---|---|---|---|
| 校门交接 | 按本校东门建批次,指定交接人,模拟少餐 | 订单状态、交接人和批次可查,未交接订单不进入校内配送 | 谁联系学生、谁登记少餐、何时补送 |
| 收餐核单 | 用小票码、手机尾号或订单号找单,重复扫码 | 至少一种方式可用,重复扫码有提示,跨站点可区分 | 模糊码、错扫和跨站点订单的处理去向 |
| 楼栋分拣 | 按校区、楼栋、宿舍分组,临时改址 | 分拣列表与实物标签一致,改址后分组同步 | 楼栋简称不一致、地址改派和错楼登记 |
| 批量送达 | 一趟多单批量更新并通知,保留未完成单 | 只更新实际完成订单,未完成保留为部分完成 | 学生未取、通知重发和二次配送 |
| 退款对账 | 正常单、部分退款、结算后退款各走一笔 | 原订单、退款记录、结算调整可关联 | 优惠承担、骑手收益冲回和商家应结变化 |
四、售后、部署与结算写进合同
售后把受理渠道、服务时段、故障分级、响应与升级路径、节假日安排和培训交付物写清。部署按标准租用、私有化、源码或定制分别核对账号主体、数据导出字段、源码范围、升级维护和迁移安排;报价把一次性费用与续费分开,历史成交价只作为带条件的案例。结算用正常单、部分退款和一次提现联测,核对用户实付、商家应结、骑手收益、平台抽成和退款冲回是否在同一组记录中对应。现场能复现、能解释、能写入合同的方案,再进入下一轮比较。
五、常见追问
问:服务商只给截图、不给证书编号怎么办?
答:先把该项列为待核验,要求提供编号、软件名称、著作权人和登记信息,再按公开登记系统比对;截图只作线索。
问:演示账号要开几个角色?
答:至少准备用户、商家、骑手和平台管理四类账号,再用两个校区账号检查订单、结算和导出是否隔离。
问:为什么状态机和唯一约束值得看?
答:重复点击、重复回调、多个骑手接同一单、批量送达部分完成,都会在这些约束下暴露;服务商能现场解释并给出处理路径,验收更有依据。
问:分账可追溯怎么验?
答:走一笔正常单、一笔部分退款和一笔结算后退款,核对原订单、退款记录和结算记录能否互相对应,演示金额只作样例。
把本校校门规定、楼栋布局、首批商家和异常订单清单整理成一页验收需求,要求服务商按上述状态、批次和退款路径现场走一遍,并记录预期输出与实际输出。能复现、能解释、能写入合同的方案,才值得进入签约前的下一轮核对。
作者:微订(上海逊柯计算机科技有限公司)。