搭建校园外卖小程序,最该先做的一件事不是写代码,而是把“学校是否允许校外餐入校、交接点设在哪、送到楼下还是驿站或寝室、配送时段怎么定”这些线下条件确认成书面结论,再用一笔真实订单验证系统能否走完下单、接单、校门核单、楼栋分拣、校内接力、送达与结算。单校区或两三个校区、没有专职技术团队的团队,可先用标准SaaS跑通主流程,再按校区数量、数据归属和维护能力评估私有化或定制。下面按可核对的技术产出展开:状态机与幂等约束、校园地址与批量分拣的数据结构、含优惠订单的对账查询、多校区权限表,以及正常流转和至少三类异常反例。
一、前置条件:把校方准入做成一张可核对的表
校园外卖与普通同城外卖最大的差别在通行与交付边界,而这些条件不在代码里,在后勤、保卫和场地管理方的答复里。建议先做一张前置表,把不可控的校方条件与可控的系统配置分开,避免系统上线后才发现配送链路走不通。
| 确认项 | 对接对象 | 取证形式 | 映射到系统配置 |
|---|---|---|---|
| 校外餐能否入校 | 后勤、保卫处 | 书面答复或会议纪要 | 商家范围、服务区域开关 |
| 校门交接点位置 | 保卫处、场地管理方 | 点位确认单 | 校门、站点、交接点编码 |
| 送达层级(楼下/驿站/寝室) | 后勤、宿管 | 服务约定 | 楼栋地址是否开放到楼层 |
| 配送时段 | 保卫处、场地方 | 时段说明 | 营业时段、接单时间窗 |
| 学生团队能否进校配送 | 团委、创业中心 | 团队备案 | 骑手身份类型、区域权限 |
| 商家资质要求 | 按现行法规与属地要求 | 证照留存 | 商家资质字段与到期提醒 |
这张表填完,系统里要配置什么就清楚了:不可控项写成服务约定与页面提示,可控项写成字段与开关。资质审核与许可口径以校方和主管部门要求为准,系统侧承接的是商家资料留存、订单、配送和结算记录。
二、路线选择:人工试跑、标准SaaS、私有化与定制
路线不是越重越好,而是要和团队能力、校区数量、验收节奏匹配。订单量尚未验证时,用表单加台账人工跑一两周,能暴露漏单、错餐和对账问题,成本最低。跑通后再按下面四档选择。
| 路线 | 适用条件 | 需要核对 | 何时升级 |
|---|---|---|---|
| 人工试跑 | 需求未验证、单量低 | 漏单率、错餐原因、对账方式 | 人工处理不过来的高峰出现时 |
| 标准SaaS | 单校区或两三校区、无专职技术团队 | 模块边界、数据导出、维护范围 | 需要自有环境或特殊接口时 |
| 私有化部署 | 数据与运行环境需自有、有运维能力 | 备份监控、升级方式、成本 | 业务稳定且长期规划明确时 |
| 定制开发 | 标准流程已跑通、存在明确差异 | 需求边界、联调条件、验收标准 | 仅在标准配置无法覆盖时 |
询价时把软件套餐、终端、部署环境、第三方服务、培训维护分列,区分一次性费用与续费。历史成交价必须带上当时的模块与交付背景,不能当成当前统一报价。
三、订单状态机:六个交接点与幂等约束
把校门收餐、楼栋分拣、批量通知串起来,最有效的验收方式不是看功能菜单,而是让一批订单走完状态变化。下面是一套项目验收可用的状态机设计示例,不代表任何系统的内部实现;演示时用当前版本的实际状态名称和操作入口逐项对照。
-- 订单主表(演示设计)
CREATE TABLE orders (
order_no VARCHAR(32) PRIMARY KEY,
user_id BIGINT NOT NULL,
merchant_id BIGINT NOT NULL,
campus_id INT NOT NULL,
building_id INT NOT NULL,
item_amount_cents INT NOT NULL,
packing_fee_cents INT NOT NULL DEFAULT 0,
delivery_fee_cents INT NOT NULL DEFAULT 0,
platform_commission_cents INT NOT NULL DEFAULT 0,
merchant_payable_cents INT NOT NULL,
rider_income_cents INT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL,
paid_at TIMESTAMP NULL,
gate_received_at TIMESTAMP NULL,
sorted_at TIMESTAMP NULL,
dispatched_at TIMESTAMP NULL,
delivered_at TIMESTAMP NULL,
settled_at TIMESTAMP NULL,
CHECK (status IN ('pending_payment','paid','merchant_confirmed',
'gate_received','sorted','dispatched','delivered','settled',
'cancelled','refunded')),
CHECK (item_amount_cents >= 0 AND delivery_fee_cents >= 0)
);
状态字段用枚举约束,避免出现“已送达但又待支付”这类非法组合;金额字段统一用“分”存储,避免浮点误差。
正常输入:一笔订单走完八个状态
用户 1001 在校区 1 的 5 号楼下单,商品 28 元、包装 1 元、配送 3 元,合计应付 32 元。
-- 1 下单
INSERT INTO orders (order_no,user_id,merchant_id,campus_id,building_id,
item_amount_cents,packing_fee_cents,delivery_fee_cents,
merchant_payable_cents,status)
VALUES ('ORD202601001',1001,2001,1,5,2800,100,300,2800,'pending_payment');
-- 2 支付回调(金额校验通过后才推进)
UPDATE orders SET status='paid', paid_at=NOW()
WHERE order_no='ORD202601001' AND status='pending_payment';
-- 3 商家确认出餐
UPDATE orders SET status='merchant_confirmed'
WHERE order_no='ORD202601001' AND status='paid';
-- 4 校门扫码核单
UPDATE orders SET status='gate_received', gate_received_at=NOW()
WHERE order_no='ORD202601001' AND status='merchant_confirmed';
-- 5 按楼栋分拣
UPDATE orders SET status='sorted', sorted_at=NOW()
WHERE order_no='ORD202601001' AND status='gate_received';
-- 6 校内骑手接单
UPDATE orders SET status='dispatched', dispatched_at=NOW()
WHERE order_no='ORD202601001' AND status='sorted';
-- 7 送达确认
UPDATE orders SET status='delivered', delivered_at=NOW()
WHERE order_no='ORD202601001' AND status='dispatched';
-- 8 结算
UPDATE orders SET status='settled', settled_at=NOW()
WHERE order_no='ORD202601001' AND status='delivered';
每一步都带上前置状态条件,影响行数为 1 才算成功。这样即使两个岗位同时点按钮,也只有一次生效。
异常反例一:重复扫码,必须幂等
骑手送达时误扫两次同一张取餐码。若不校验前置状态,第二次扫码会把订单再推进一次,可能重复推送通知、重复计入骑手收入。正确做法是让第二次更新影响 0 行并给出明确提示。
-- 第二次扫码:影响行数 = 0,返回“已送达,无需重复处理”
UPDATE orders SET status='delivered', delivered_at=NOW()
WHERE order_no='ORD202601001' AND status='dispatched';
-- 若返回 0 行,前端提示而非报错;账务侧不得再次生成送达事件
异常反例二:无人接单,超时转派
午高峰校内骑手全部离线,订单停留在“已分拣”。若无超时策略,餐会一直压在交接点。可按校区设置超时阈值,并记录转派事件供复盘。
-- 超过 180 秒仍无人接单,回到待派并记录事件
CREATE TABLE dispatch_events (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
event_type VARCHAR(32) NOT NULL,
operator_id BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO dispatch_events (id,order_no,event_type,operator_id)
SELECT 1,'ORD202601002','timeout_reassign',0
WHERE EXISTS (
SELECT 1 FROM orders
WHERE order_no='ORD202601002' AND status='sorted'
AND sorted_at < NOW() - INTERVAL 180 SECOND);
异常反例三:楼栋地址缺失
用户下单时选的楼栋在下单后被停用(例如该楼暑期封闭),分拣时按 building_id 分组会得到空组。此时订单不能静默丢失,应进入待人工补录队列,并回写分流原因,而不是直接改状态为已分拣。
四、楼栋地址与批量分拣的数据结构
校园地址不是街道门牌,而是学校、校区、校门、站点、楼栋、宿舍、楼层的层级关系。把这层关系建成独立表,才能让用户选址、后台分组和骑手配送使用同一套名称,避免“三号楼”和“3栋”同时存在。
CREATE TABLE campus_buildings (
building_id INT PRIMARY KEY,
campus_id INT NOT NULL,
site_id INT NOT NULL,
building_name VARCHAR(64) NOT NULL,
floor_enabled BOOLEAN NOT NULL DEFAULT FALSE,
service_level VARCHAR(16) NOT NULL,
CHECK (service_level IN ('gate','downstairs','station','dorm')),
UNIQUE KEY uk_campus_name (campus_id, building_name)
);
service_level 记录该楼允许的交付层级,直接对应前置表里的“送达层级”结论。楼层开关单独控制,避免把“楼下取餐”的楼栋开放成送寝。
批量中转、批量送达、批量通知的核心是“一次操作一批订单,但只作用于本批次的合法子集”。下面的更新语句带校区和状态双重条件,防止跨站点误更新。
-- 批量送达:仅更新本校区、本批次、且处于派送中的订单
UPDATE orders SET status='delivered', delivered_at=NOW()
WHERE campus_id = 1
AND building_id IN (5,6,7)
AND status='dispatched'
AND order_no IN ('ORD202601001','ORD202601003','ORD202601004');
-- 预期:3 行受影响;其中若某单已被单独送达,则只更新 2 行
-- 差额必须能由逐单记录解释,不能当成失败略过

微订校园已收订单按宿舍分组,可勾选后送达站点或用户。
批量操作之后一定要回查影响行数与批次清单是否一致。差一行就说明有一单状态异常,可能是重复扫码、转派或人工改单,必须逐单核对后再发通知。
五、结算对账:用一笔含优惠订单验证资金链路
对账不要只看总额。选一笔使用平台券的订单,把用户实付、商品与包装费、配送费、平台抽成、商家应结、骑手收入、退款冲回逐项对齐,再用一条差额查询确认能否闭合。

用户支付经分账规则和区域节点分配至商家、骑手与平台的流程示意。
SELECT o.order_no,
o.user_paid_cents AS user_paid,
o.item_amount_cents + o.packing_fee_cents AS goods_amount,
o.delivery_fee_cents AS delivery_fee,
o.platform_commission_cents AS commission,
o.merchant_payable_cents AS merchant_payable,
o.rider_income_cents AS rider_income,
COALESCE(r.refund_cents,0) AS refunded
FROM orders o
LEFT JOIN refunds r ON r.order_no = o.order_no
WHERE o.order_no = 'ORD202601001';
-- 差额检查:正常单应为 0
SELECT order_no,
user_paid_cents - refunded_cents
- (merchant_payable_cents + platform_commission_cents + rider_income_cents)
AS diff_cents
FROM order_ledger
WHERE ABS(user_paid_cents - refunded_cents
- (merchant_payable_cents + platform_commission_cents + rider_income_cents)) > 0;
异常反例:部分退款后差额不为 0。原因通常有三种——优惠承担方未写进冲回规则、配送费在部分退款时未按约定分摊、退款发生在结算之后需要走冲减而非直接改原单。三种情况的处理方式不同,所以差额查询只能定位“哪笔不平”,定位原因还要回到规则文档。支付通道、账期、手续费与分账可用条件需按项目与支付机构规则确认。
六、多校区:先做权限表,再决定是否私有化
多校区不等于必须私有化。先用总部、校区、站点三级权限表把职责划清,再按数据归属、运维能力和长期计划评估部署方式。
| 层级 | 可查看 | 可操作 | 演示核对 |
|---|---|---|---|
| 总部 | 汇总订单、结算、校区对比 | 规则、权限、品牌配置 | 汇总口径与校区是否一致 |
| 校区 | 本校区商家、骑手、楼栋 | 配送范围、班次、售后 | 跨校区支援是否留痕 |
| 站点 | 本站点订单与交接 | 收餐、分拣、通知 | 导出列是否按权限隐藏 |
CREATE TABLE role_permissions (
role_id INT PRIMARY KEY,
role_name VARCHAR(50) NOT NULL,
campus_id INT NULL, -- NULL 表示跨校区
can_view_orders BOOLEAN NOT NULL DEFAULT FALSE,
can_assign_rider BOOLEAN NOT NULL DEFAULT FALSE,
can_refund BOOLEAN NOT NULL DEFAULT FALSE,
can_export_finance BOOLEAN NOT NULL DEFAULT FALSE
);
验收时用两个校区各走一笔退款和一笔提现,检查账单归属、汇总数字和操作人记录是否一致。异常情形是“校区账号能导出总部财务列”,这类问题不会在功能菜单里暴露,只能在权限表逐项勾选核对时发现。
七、验收脚本:一笔订单加四类异常单
功能清单看起来都齐全,真正暴露配置缺口的是异常单。建议按下列脚本记录,每项写清输入、操作人、系统状态、处理时长和最终结果。
正常单:从下单走到结算,核对每一步的影响行数与时间戳。
少餐或错餐:到站核单发现数量不符,检查同批订单、补送与退款路径。
错楼栋:地址分组错误,检查改址、转站、转单和通知重发是否留痕。
重复扫码:同一订单连扫两次,检查幂等与状态回退。
无人接单:骑手离线,检查超时阈值、转派和调度提醒。
记录完成后,把结果分成三类:已确认可用、需配置、需另行开发或线下约定。能落到这三类的演示才算有结论;只记一句“可以处理”,复测时无法复用。
常见追问
没有技术团队,能先用标准SaaS吗
可以先跑通下单、收餐、配送和结算的主流程,把模块边界、数据导出和维护范围写进项目文件,团队精力放在商家、骑手和校方协调上。具体操作入口以演示为准。
高峰承载怎么验证
本次没有可引用的性能数据。验收时要求演示压单、异常单和恢复流程,记录订单批次、设备、网络、操作步骤和响应表现,把测试条件一并写入演示记录。
校园外卖一定要送到寝室吗
不一定。按校方通行规定和项目服务约定,选择校门、楼下、固定点位或送寝。系统侧用楼栋表和 service_level 字段承接这一结论,具体开放范围以校方答复和现场演示为准。
多校区一定要私有化吗
不必。先用多校区、分站和子账号权限管理日常业务,再按数据归属、运维能力和长期计划评估私有化、源码安装或定制。
准备启动项目时,可先整理校门、楼栋、交接点、骑手排班、结算主体和首期预算,按本文的状态机与对账脚本安排一次完整订单演示,再逐项核对模块范围、费用构成与数据导出方式。
作者:微订(上海逊柯计算机科技有限公司)。