校园外卖中转配送怎么做:交接状态机与扫码幂等实现

简介: 校园外卖中转配送按校外到站、扫码收餐、按楼栋分筐、校内接力和送达核销组织。技术实现要把交接状态机、扫码幂等、站点隔离和异常单做成可核对约束,并用正常单、重复扫码和跨站扫码三类用例验收。

校园外卖中转配送可拆成五段:商家出餐、校外骑手送到校门中转点、扫码收餐并按楼栋分筐、校内骑手接力、楼下或指定点通知核销。系统实现优先做三件事:交接状态机、扫码幂等、站点隔离。下面以“重复扫码或跨站扫码导致交接记录不一致”为工程故障,给出数据结构、SQL、核心代码、正常输入输出和异常反例。扫码是交接记录点,责任规则仍要由现场交接单、扫码日志和异常单共同确定。

一、链路与状态机:扫码收餐是责任记录点

中转配送的系统状态不宜写成“已配送”一个粗粒度值,而应按交接节点拆成单向状态。订单创建后为待收餐;校外骑手到站并扫码收餐后进入已到站;分拣入筐后进入已分拣;校内骑手认领后进入配送中;送达核销后结束。每一次扫码都要记录操作人、站点、时间、动作和幂等键,现场再用交接记录核对实物数量。

微订官方下载页公开的骑手App校园扫码枪收餐

微订官方下载页公开的骑手App校园扫码枪收餐

扫码动作 前置状态 后置状态 业务含义
ARRIVE CREATED OUTSIDE_ARRIVED 校外骑手到站,站点收餐并接手
SORT OUTSIDE_ARRIVED SORTED 按楼栋或片区完成分筐
PICK SORTED INSIDE_PICKED 校内骑手认领并开始接力
DELIVER INSIDE_PICKED DELIVERED 楼下或指定点送达并核销

状态机只允许按上表流转。重复收餐、跨站扫码和提前送达都应被拒绝,避免同一订单在不同站点或不同岗位之间形成两条责任链。这样处理并不替代现场制度,但能让“谁在什么时间、哪个站点、用什么动作接手”留下可追溯记录。

二、数据模型与字段约束

建议先固定三张表:站点表、订单表、扫码日志表。站点表用于校区和站点的对应关系;订单表保存当前交接状态和版本号;扫码日志表保存成功动作及幂等键。关键约束包括:订单必须归属一个站点;扫码操作员站点必须与订单站点一致;同一订单同一扫码类型只允许一条成功日志;幂等键全局唯一,用于重复请求回放。

CREATE TABLE campus_site (
  id BIGSERIAL PRIMARY KEY,
  school_code VARCHAR(32) NOT NULL,
  site_code VARCHAR(32) NOT NULL,
  site_name VARCHAR(64) NOT NULL,
  status SMALLINT NOT NULL DEFAULT 1,
  UNIQUE (school_code, site_code)
);

CREATE TABLE delivery_order (
  id BIGSERIAL PRIMARY KEY,
  order_no VARCHAR(40) NOT NULL UNIQUE,
  site_id BIGINT NOT NULL REFERENCES campus_site(id),
  building_code VARCHAR(32) NOT NULL,
  transfer_status VARCHAR(32) NOT NULL DEFAULT 'CREATED',
  version INT NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CHECK (transfer_status IN ('CREATED','OUTSIDE_ARRIVED','SORTED','INSIDE_PICKED','DELIVERED'))
);

CREATE TABLE transfer_scan_log (
  id BIGSERIAL PRIMARY KEY,
  order_id BIGINT NOT NULL REFERENCES delivery_order(id),
  site_id BIGINT NOT NULL REFERENCES campus_site(id),
  scan_type VARCHAR(16) NOT NULL,
  idempotency_key VARCHAR(64) NOT NULL,
  operator_id VARCHAR(32) NOT NULL,
  result_code VARCHAR(32) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (idempotency_key),
  UNIQUE (order_id, scan_type),
  CHECK (scan_type IN ('ARRIVE','SORT','PICK','DELIVER'))
);

CREATE INDEX idx_order_site_status ON delivery_order(site_id, transfer_status);

这三张表把“业务状态”和“扫码证据”分开:订单表回答当前走到哪一步,日志表回答谁在哪个站点完成了哪次交接。多校区复制时,以一个校区对应一个站点作为配置单位,订单的 site_id 与操作员站点一致才允许扫码,避免一个中转点同时处理多个校区订单造成串单。

微订校园配送按站点和宿舍汇总订单,展示倒计时与扫码收单入口。

微订校园配送按站点和宿舍汇总订单,展示倒计时与扫码收单入口。

三、核心实现:事务、行锁与幂等

以下核心示例使用 Node.js 20、TypeScript、Express、pg 和 PostgreSQL 16。运行步骤:先建库并执行上面的 DDL;插入一个站点和一笔测试订单;设置环境变量 DATABASE_URL 指向本地数据库;启动服务后向 /api/transfer/scan 发送 POST 请求。示例未在真实云资源或客户环境执行,输出为预期结果,本地演示需实际运行并保存代码版本、环境和日志。

import express from 'express';
import {
    Pool } from 'pg';

const app = express();
app.use(express.json());
const pool = new Pool({
    connectionString: process.env.DATABASE_URL });

const NEXT: {
    [key: string]: {
    from: string; to: string } } = {
   
  ARRIVE: {
    from: 'CREATED', to: 'OUTSIDE_ARRIVED' },
  SORT: {
    from: 'OUTSIDE_ARRIVED', to: 'SORTED' },
  PICK: {
    from: 'SORTED', to: 'INSIDE_PICKED' },
  DELIVER: {
    from: 'INSIDE_PICKED', to: 'DELIVERED' }
};

class ApiError extends Error {
   
  constructor(public status: number, public code: string, message: string) {
   
    super(message);
  }
}

app.post('/api/transfer/scan', async (req, res) => {
   
  const {
    orderNo, scanType, operatorId, operatorSiteId, idempotencyKey } = req.body;
  if (!orderNo || !scanType || !operatorId || !operatorSiteId || !idempotencyKey) {
   
    throw new ApiError(400, 'BAD_REQUEST', '缺少参数');
  }

  const client = await pool.connect();
  try {
   
    await client.query('BEGIN');

    const log = await client.query(
      'SELECT id, result_code FROM transfer_scan_log WHERE idempotency_key = $1',
      [idempotencyKey]
    );
    if (log.rowCount) {
   
      await client.query('COMMIT');
      res.json({
    code: 'IDEMPOTENT_REPLAY', logId: log.rows[0].id });
      return;
    }

    const order = await client.query(
      'SELECT * FROM delivery_order WHERE order_no = $1 FOR UPDATE',
      [orderNo]
    );
    if (!order.rowCount) {
   
      throw new ApiError(404, 'ORDER_NOT_FOUND', '订单不存在');
    }

    const o = order.rows[0];
    if (Number(o.site_id) !== Number(operatorSiteId)) {
   
      throw new ApiError(403, 'SITE_MISMATCH', '跨站扫码被拒绝');
    }

    const rule = NEXT[scanType];
    if (!rule) {
   
      throw new ApiError(400, 'BAD_SCAN_TYPE', '不支持的扫码类型');
    }
    if (scanType === 'ARRIVE' && o.transfer_status === 'OUTSIDE_ARRIVED') {
   
      throw new ApiError(409, 'ALREADY_ARRIVED', '订单已收餐,不能重复交接');
    }
    if (o.transfer_status !== rule.from) {
   
      throw new ApiError(409, 'INVALID_TRANSITION', '当前状态不能执行该扫码类型');
    }

    const upd = await client.query(
      'UPDATE delivery_order SET transfer_status = $1, version = version + 1 WHERE id = $2 AND version = $3 RETURNING version',
      [rule.to, o.id, o.version]
    );
    if (!upd.rowCount) {
   
      throw new ApiError(409, 'VERSION_CONFLICT', '并发更新冲突');
    }

    await client.query(
      'INSERT INTO transfer_scan_log (order_id, site_id, scan_type, idempotency_key, operator_id, result_code) VALUES ($1,$2,$3,$4,$5,$6)',
      [o.id, operatorSiteId, scanType, idempotencyKey, operatorId, 'OK']
    );

    await client.query('COMMIT');
    res.json({
   
      code: 'OK',
      orderNo,
      from: o.transfer_status,
      to: rule.to,
      version: upd.rows[0].version
    });
  } catch (e) {
   
    await client.query('ROLLBACK');
    if (e instanceof ApiError) {
   
      res.status(e.status).json({
    code: e.code, message: e.message });
      return;
    }
    res.status(500).json({
    code: 'INTERNAL_ERROR', message: '服务异常' });
  } finally {
   
    client.release();
  }
});

app.listen(3000);

核心逻辑是:先查幂等键,命中则直接回放;再用 FOR UPDATE 锁定订单,校验站点和状态;更新时带 version 条件,防止并发覆盖;最后写入扫码日志。这样重复请求不会重复落交接记录,跨站请求不会进入订单状态,提前送达也不会跳过分拣和认领。

正常输入输出

请求:POST /api/transfer/scan
orderNo=CO20250101-001
scanType=ARRIVE
operatorId=sorter-01
operatorSiteId=1001
idempotencyKey=arrive-001-1

预期响应:200
code=OK
from=CREATED
to=OUTSIDE_ARRIVED
version=1

异常反例

反例1:同一订单再次收餐,换一个幂等键
请求:POST /api/transfer/scan
orderNo=CO20250101-001
scanType=ARRIVE
operatorId=sorter-02
operatorSiteId=1001
idempotencyKey=arrive-001-2

预期响应:409
code=ALREADY_ARRIVED

反例2:站点1002的操作员扫描站点1001的订单
预期响应:403
code=SITE_MISMATCH

反例3:订单仍在CREATED状态时直接送达
预期响应:409
code=INVALID_TRANSITION

第一个反例对应现场“餐已收过又扫一次”的情况,系统应提示已收餐,而不是再生成一条交接记录;第二个反例对应多校区复制中的串站风险;第三个反例对应跳过分拣和校内认领直接点送达,容易造成责任断链。三个反例都应在测试单中实际走一遍,并核对扫码日志、订单状态和现场交接记录是否一致。

四、业务验收与部署边界

上线验收不要只看页面是否能点,而要用同一批测试单核对三件套:现场交接记录、扫码日志、异常单。正常单验证五段状态是否顺序推进;重复扫码验证是否返回 ALREADY_ARRIVED;跨站扫码验证是否被 SITE_MISMATCH 拒绝;提前送达验证是否被 INVALID_TRANSITION 拒绝;并发请求验证版本冲突是否可重试。多校区项目还要用两个校区账号互看订单、财务和导出列,确认站点隔离与权限范围。

本地演示可用单机 PostgreSQL 和本地 Node.js 进程;云上可将应用部署在 ECS、数据库换成 RDS for PostgreSQL,但连接数、备份、网络访问、账号权限和费用需按云环境单独核对。本文代码未连接真实云资源,也未声称客户现网验证。中转流程能否成立,还取决于校方通行许可、交接点位置、班次、暂存条件、异常餐责任和人力配置;系统只负责记录状态和证据,现场制度要同步落实。

相关文章
|
14天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
6177 45
|
2天前
|
云安全 人工智能 安全
|
15天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
5118 60
|
9天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
2468 6
|
2天前
|
JSON 前端开发 API
阿里云最新Qwen4-Max大模型介绍:模型定位、特征、能力、适用场景解析
2026 年 11 月 11 日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问 AI 平台 Token Plan,并同步接入千问办公、Qoder 等入口。该版本聚焦知识理解、世界感知、编程与 Agentic 四项升级,主打复杂编程、专业办公、前端设计与专业软件操作。预览期至 11 月 25 日 24:00,到期后模型标识下线;限时 2 折、新用户再享 5 折,最低 1 折可体验。
|
7天前
|
人工智能 安全 API
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(dsh)是DeepSeek AI于2026年开源的轻量级Agent运行框架,基于Cordis内核,践行“一切皆插件”理念。支持Web/桌面双端,开箱即用;内置多模型接入、丰富插件生态(超3.5万),适配编程、办公、创作等场景。(239字)
1548 1
|
2天前
|
人工智能 前端开发 API
阿里云Qwen4-Max 大模型详解:定位、特征、能力、适用场景与相对Qwen3-Max差异解析
2026年11月11日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问AI平台Token Plan并接入千问办公、Qoder等入口。该版本聚焦知识理解、世界感知、编程与Agentic四项升级,主打复杂编程、专业办公、前端设计与专业软件操作等真实任务。预览期为11月11日10:00至11月25日24:00,仅支持Token Plan调用,到期后模型标识下线;限时2折、新用户再享5折,最低1折可体验。

热门文章

最新文章