同城外卖小程序 上线后,研发与运营扯皮的高频句是:「用户端明明已出餐,导出怎么还是制作中?」根因通常是端展示、订单进度、导出快照三层不一致,而不是单纯「导出按钮坏了」。

本文讲:下单成功后订单进度如何单点推进;导出字段如何与端上枚举对齐;如何避免用户端本地抢先改状态导致对账失败。
结论:一个进度源,导出读同一枚举
text
payment-result -> order-progress (权威)
|
+-----------+-----------+
v v v
mini-app merchant-app export-job
(display) (display) (same enum)
展示文案可以因端而异,但 status_code 必须同源。导出禁止读「端本地缓存」或「另一张手工表」。
进度枚举与文案分离
python
STATUS = {
"pending_pay": {"user": "待付款", "merchant": "待付款"},
"pending_accept": {"user": "待商家接单", "merchant": "新订单"},
"preparing": {"user": "制作中", "merchant": "制作中"},
"ready": {"user": "待取餐", "merchant": "已出餐"},
"done": {"user": "已完成", "merchant": "已完成"},
}
def render_status(code: str, side: str) -> str:
return STATUS[code][side]
导出 CSV 宜带 status_code 与 status_label,便于财务筛选,也便于和用户截图对照。
支付成功后再推进
python
def on_payment_callback(order_id: str, gateway_tx: str, success: bool):
if not success:
order_repo.mark_pay_failed(order_id, gateway_tx)
return
with order_repo.lock(order_id):
if order_repo.pay_seen(gateway_tx):
return # 幂等
order_repo.mark_paid(order_id, gateway_tx)
order_repo.update_status(order_id, "pending_accept")
audit_log.append(order_id, "paid", gateway_tx)
禁止用户端在回调到达前把本地状态写成 pending_accept;商家端列表过滤 pending_accept 时自然看不到「假成功」单。
商家出餐:单写入口
python
def merchant_mark_ready(order_id: str, merchant_id: str):
with order_repo.lock(order_id):
o = order_repo.get(order_id)
assert o.merchant_id == merchant_id
if o.status not in ("preparing", "accepted"):
raise StateError("cannot ready")
order_repo.update_status(order_id, "ready")
audit_log.append(order_id, "ready", actor=merchant_id)
用户端轮询或订阅同一 order_id 的 status_code,刷新后应看到 ready 对应文案。
导出任务:读库不读端
python
def export_orders(from_dt, to_dt, out_path: str):
rows = db.query("""
SELECT order_id, status_code, pay_amount, paid_at, ready_at, merchant_id
FROM order_main
WHERE created_at BETWEEN %s AND %s
""", (from_dt, to_dt))
for r in rows:
r["status_label"] = STATUS[r["status_code"]]["user"]
write_csv(out_path, rows)
若导出仍显示「制作中」,先查库内 status_code,再查端缓存,不要反过来以端为准改库。
审计流水:解释「用户说已出餐、导出不是」
sql
CREATE TABLE order_status_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(32) NOT NULL,
from_code VARCHAR(32),
to_code VARCHAR(32) NOT NULL,
actor VARCHAR(64),
created_at DATETIME NOT NULL,
INDEX idx_order_time (order_id, created_at)
);
客服查单时拉流水,能回答「何时从 preparing 到 ready」,比口头解释可靠。
端侧反模式
javascript
// 错误:本地先改已出餐
function onMerchantTapReady() {
ui.setReady(); // 假成功
api.post('/merchant/ready', { orderId });
}
// 正确:等服务端确认再改 UI
async function onMerchantTapReady() {
const res = await api.post('/merchant/ready', { orderId });
ui.render(res.status_code);
}
验收用例
- 真实支付试跑:商家端 30 秒内见
pending_accept - 商家点出餐:用户端刷新为
ready,导出同行 - 重复支付回调:进度不二次横跳
- 导出
status_code与后台详情页一致 - 弱网提交:无「用户成功、库内无单」
交付说明
光合同城由郑州光合科技有限公司交付同城本地生活成品系统,外卖模块可私有化部署。导出与用户端状态对齐,宜作为源码交付验收项,而不是上线后由财务手工改 Excel。
打印与厨显端
部分门店用打印或厨显代替商家 App。打印任务仍应订阅同一 order_id 状态,而不是读本地 UDP 广播:
python
def on_status_change(order_id: str, to_code: str):
if to_code == "pending_accept":
print_queue.enqueue(order_id, template="kitchen_new")
if to_code == "ready":
print_queue.enqueue(order_id, template="pickup_ready")
打印失败重试不应改订单状态;状态以库为准。
部分退款与状态
若支持部分退款,宜独立 refund_status,不覆盖 status_code 主进度:
sql
ALTER TABLE order_main ADD COLUMN refund_flag TINYINT DEFAULT 0;
-- 或 refund 子表;导出时单独一列,避免把「已完成」改写成「退款中」导致厨房误判
导出性能
大范围导出走异步 job,读只读副本:
python
def enqueue_export(from_dt, to_dt):
job_id = uuid4().hex
queue.send({"job_id": job_id, "from": from_dt, "to": to_dt})
return job_id
导出 CSV 必含 status_code 与 status_label 两列,财务筛选用 code,人工对照用 label。
压测用例补充
- 支付回调延迟 30 秒:用户端应保持待支付或处理中,商家端无新单。
- 重复回调:状态不二次跳转,审计流水只记一次 paid。
- 商家拒单:用户端可见拒单原因码,导出同行。
- 并发出餐点击:最终仍为 ready 一次,幂等生效。
交付文档应包含的状态机图
交付时附 pending_pay -> pending_accept -> preparing -> ready -> done 合法迁移表,以及「哪些动作由哪个端触发」。避免口头约定「出餐按钮可有可无」,上线后导出与端展示再次分叉。
试点门店宜留一套「支付成功 → 商家见单 → 出餐 → 用户刷新 → 导出同行」的截图或录屏,作为后续扩城时的对照标准。新接入支付通道时,重复跑同一套用例,而不是只测「能不能付款」。
客服培训材料应写清:查单先看库内 status_code,再看用户端截图时间,避免以截图为准改库内状态。
小结
同城外卖小程序从下单到出餐,后台导出能不能对上用户端状态,取决于是否只有一个进度源、支付是否先于接单、导出是否读库内枚举。把三层分开,「付了款待接单」和「导出对不上」才能在工程上闭环。