把校园外卖做成大平台一样,关键不是复刻App界面,而是先把校方准入、交付点和配送模式变成可配置参数,再用用户、商家、骑手、平台后台四端跑通同一笔订单。单校或两三个校区起步,可先用标准SaaS验证下单、接单、派单、送达、退款和对账;有跨校区数据管理或特殊接口需求时,再评估私有化、源码或定制。验收重点放在校门交接、楼栋分拣、批量通知和结算对账,而不是功能菜单数量。
校园外卖与城市外卖的第一差异在通行与交付。项目启动前,把校方规则整理成一张可配置参数表,再让系统演示按表逐项走。这样选型时比较的是同一套业务约束,而不是各说各的功能名。
参数 | 可选值 | 对系统的影响 |
|---|---|---|
准入方式 | 校外骑手入校、仅校门交接、混合模式 | 决定是否设中转站、校内接力骑手、交接扫码环节 |
交付点 | 宿舍楼下、固定取餐点、外卖柜、校门 | 决定地址层级、通知模板、签收凭证和异常暂存规则 |
通行时段 | 全天、午晚高峰、禁送时段 | 决定服务时段、调度规则和骑手排班 |
计费口径 | 按楼栋、按时段、按距离 | 决定计费配置、优惠承担和对账字段 |
责任岗位 | 站点分拣、校内骑手、客服、财务 | 决定账号权限、订单操作范围和异常升级路径 |
这张表先于系统采购。校方准入、交付点位、禁送时段和场地负责人确认后,直送、校门中转或混合模式才有配置依据。演示时让服务商用一笔正常单和一笔退款单走完整链路,比只看后台菜单更能判断适配程度。
四端不是四个孤立页面,而是同一订单号在不同角色下的视图。订单主数据至少记录订单号、校区、站点、楼栋、商家、商品明细、用户实付、配送费、优惠承担、交付方式、骑手、状态时间和异常原因。四端共用同一订单号,履约与账务才能对上。
角色 | 核心操作 | 关键数据 | 验收点 |
|---|---|---|---|
用户端 | 选楼栋、下单、支付、催单、售后 | 地址、订单、支付单 | 楼栋可选,状态可见 |
商家端 | 接单、出餐、缺货处理、对账 | 商品、订单、结算单 | 接单提醒,缺货可流转 |
骑手端 | 接单、取餐、交接、送达 | 任务、骑手收入 | 交接确认,异常可上报 |
平台端 | 审核、调度、权限、结算 | 商家、骑手、订单、账单 | 权限隔离,账单可导出 |
状态机把“谁在什么条件下能把订单推到下一步”写成约束,异常单才有统一入口。以下为验收脚本的简化模型,用于说明状态约束和事件记录,不代表任何产品内部实现。
from dataclasses import dataclass
from typing import Dict, Set
ALLOWED: Dict[str, Set[str]] = {
'created': {'paid', 'cancelled'},
'paid': {'accepted', 'cancelled', 'refund_pending'},
'accepted': {'preparing', 'cancelled', 'refund_pending'},
'preparing': {'ready', 'refund_pending'},
'ready': {'at_gate', 'refund_pending'},
'at_gate': {'sorted', 'refund_pending'},
'sorted': {'delivering', 'refund_pending'},
'delivering': {'delivered', 'refund_pending'},
'delivered': {'settled', 'refund_pending'},
'refund_pending': {'refunded', 'delivered'},
'refunded': {'settled'},
'settled': set(),
'cancelled': set(),
}
def can_transition(src: str, dst: str) -> bool:
return dst in ALLOWED.get(src, set())
@dataclass
class OrderEvent:
order_id: str
src: str
dst: str
operator: str
reason: str = ''
def apply_event(event: OrderEvent) -> dict:
if not can_transition(event.src, event.dst):
raise ValueError(f'illegal transition: {event.src} -> {event.dst}')
return {
'order_id': event.order_id,
'status': event.dst,
'operator': event.operator,
'reason': event.reason,
}
print(apply_event(OrderEvent('SO1001', 'created', 'paid', 'user')))
print(apply_event(OrderEvent('SO1001', 'paid', 'accepted', 'merchant')))预期结果是两次转换均返回状态字典;若从created直接跳到delivered,脚本抛出异常。验收时把取消、退款、转派和异常上报都按同样方式写成事件,检查四端看到的状态名称是否一致。
把校门到宿舍拆成六段:商家出餐、校外运输、校门交接、按楼栋分拣、校内骑手接力、送达通知。每段写清订单状态、操作人、时间戳和异常处理。高峰时按校区、站点、楼栋筛选订单,再批量中转、批量送达和批量通知;少餐、错餐、联系不上用户等订单进入单独队列,由专人处理。
分拣的核心是稳定分组,而不是人工记楼栋。下面的简化函数把订单按校区、楼栋和站点归组,输出排序后的订单号列表,便于站点按批次交接。
from collections import defaultdict
def group_by_building(orders):
buckets = defaultdict(list)
for order in orders:
key = (order['campus_id'], order['building_id'], order.get('station_id', ''))
buckets[key].append(order['order_id'])
return {key: sorted(ids) for key, ids in sorted(buckets.items())}
def build_notice_payload(batch_id, orders, template):
payload = []
for order in orders:
payload.append({
'idempotency_key': '{}:{}:arrived'.format(batch_id, order['order_id']),
'order_id': order['order_id'],
'user_id': order['user_id'],
'template': template,
'building_id': order['building_id'],
})
return payload
orders = [
{'order_id': 'SO1001', 'campus_id': 'campus_01', 'building_id': 'bldg_03', 'station_id': 'stn_01', 'user_id': 'u1001'},
{'order_id': 'SO1002', 'campus_id': 'campus_01', 'building_id': 'bldg_03', 'station_id': 'stn_01', 'user_id': 'u1002'},
{'order_id': 'SO1003', 'campus_id': 'campus_01', 'building_id': 'bldg_01', 'station_id': 'stn_01', 'user_id': 'u1003'},
]
print(group_by_building(orders))预期输出如下。批量通知必须带批次号和幂等键,避免同一订单因重复点击或消息重试被多次通知;改址、缺餐和退款订单单独处理,不混入正常批次。
{('campus_01', 'bldg_01', 'stn_01'): ['SO1003'], ('campus_01', 'bldg_03', 'stn_01'): ['SO1001', 'SO1002']}部署方式按阶段选择,而不是按功能名比较。单校起步先用标准SaaS跑通订单,订单稳定、数据管理要求提高后再评估私有化或源码;只有标准模块覆盖不了的接口或流程才做定制。询价时把软件模块、终端、部署、第三方服务、定制、培训和维护分项列出,历史成交价只作带配置背景的参考。
路径 | 适合阶段 | 核对重点 |
|---|---|---|
标准SaaS | 单校或两三个校区试运营 | 模块范围、账号权限、数据导出 |
私有化部署 | 订单稳定、需自有环境 | 备份、监控、升级与维护分工 |
源码安装 | 有技术团队、需长期维护 | 交付文档、运行环境、许可与升级 |
个性化开发 | 标准流程外有明确接口需求 | 需求范围、联调、测试与后续维护 |
结算对账先把字段拆开:用户实付、商品款、包装费、配送费、平台抽成、商家应结、骑手收入、平台优惠承担、商家优惠承担和退款冲回。以下SQL为字段核对示例,不对应任何产品表结构。
SELECT
order_id,
user_paid,
goods_amount,
delivery_fee,
platform_commission,
merchant_payable,
rider_income,
platform_discount,
merchant_discount,
refund_amount
FROM order_settlement
WHERE campus_id = :campus_id
AND settle_date = :settle_date;核对时用一笔正常单、一笔部分退款和一笔结算后退款走一遍,确认原订单、退款单、商家账单和骑手收入能相互关联。支付通道、主体资质、账期和提现规则按项目方案确认,避免把某一项目的费率当成通用报价。
上线前不要只演示“能下单”,要用五类订单覆盖校园履约的分歧点。每类订单记录操作账号、状态变化、通知结果和账单变化,发现问题后修正配置再重跑。
订单类型 | 重点检查 | 通过标准 |
|---|---|---|
正常单 | 下单、接单、出餐、校门交接、楼栋分拣、送达通知 | 四端状态一致,通知到达正确取餐点 |
缺餐单 | 少餐登记、补送或退款、客服跟进 | 异常有责任人,原订单可追溯 |
错楼栋单 | 地址修改、分拣组调整、骑手任务变更 | 分组和通知同步更新,不重复扣费 |
临时改址单 | 改址后的费用、交接点和通知模板 | 新旧地址不混单,账单有调整记录 |
退款对账单 | 部分退款、结算后退款、优惠冲回 | 商家应结和骑手收入可解释 |
验收脚本可复用前面的状态机:先断言合法转换通过,再断言非法转换被拒绝;分拣函数用跨楼栋样本检查分组,通知函数检查同一批次不产生重复幂等键。没有实际运行记录时,只写预期结果和验证方法,不把演示结果写成经营效果。
问:校园外卖一定要做校门中转吗?
答:不一定。校方允许校外骑手入校时可直送,限制入校时可用校门交接、固定取餐点或混合模式;系统按准入、交付点和通行时段配置,再按本校规则演示。
问:单校起步选SaaS还是私有化?
答:单校或两三个校区、技术团队有限时,先用标准SaaS跑通订单、退款和对账;订单稳定、数据管理要求提高后,再评估私有化或源码,具体边界按项目确认。
问:怎么验证分拣和批量通知?
答:用跨楼栋、同楼栋多单、临时改址和缺餐样本做演示,检查分组结果、通知幂等键和异常队列;批量操作要能追溯到批次号和操作人。
问:结算要核对哪些字段?
答:至少拆开用户实付、商品款、配送费、平台抽成、商家应结、骑手收入、优惠承担和退款冲回,再用正常单、部分退款和结算后退款各走一遍。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。