同城配送小程序:运单状态机与骑手调度的工程实现

简介: 本文详解同城配送系统核心工程实践:基于MySQL 8+Redis,聚焦运单状态机(条件更新防并发乱序)、抢单/派单双模式选型、分段计费与时段加价、节流轨迹上报、超时自动治理及异常件闭环处理,强调可落地、易维护的设计原则。(239字)

同城配送是这两年本地生活里增长明确的类型:水果店、药店、鲜花店都在把"商家自己找人送"换成专业运力。从工程角度看,配送系统的核心难点不在下单,而在两处:运单状态机怎么设计才不会乱,骑手调度怎么在抢单和派单之间做取舍。本文给出可落地的实现,基于 MySQL 8 + Redis。

一、运单状态机:把非法流转挡在数据库层

配送运单的典型生命周期是:

待接单 → 已接单 → 已取货 → 配送中 → 已送达
                   ↓                 ↓
                 已取消            已妥投异常

最容易犯的错误是把状态更新写成"无条件赋值":UPDATE order SET status = 3 WHERE id = ?。并发场景下,骑手取消和用户取消可能同时到达,配送中订单被改回待接单,运力就丢了。正确做法是把"从哪个状态来"写进 WHERE 条件:

CREATE TABLE delivery_order (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no      VARCHAR(32) NOT NULL,
  pickup_addr   VARCHAR(200) NOT NULL,
  drop_addr     VARCHAR(200) NOT NULL,
  pickup_lat DECIMAL(10,6), pickup_lng DECIMAL(10,6),
  drop_lat   DECIMAL(10,6), drop_lng   DECIMAL(10,6),
  fee           INT NOT NULL COMMENT '运费,单位分',
  rider_id      BIGINT NULL,
  status        TINYINT NOT NULL DEFAULT 1,
  status_detail VARCHAR(200) NULL,
  promised_at   DATETIME NULL COMMENT '承诺送达时间',
  finished_at   DATETIME NULL,
  created_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_order_no (order_no),
  KEY idx_rider_status (rider_id, status),
  KEY idx_status_created (status, created_at)
);
所有状态流转走统一的条件更新:
// 状态常量
const S = { CREATED:1, ACCEPTED:2, PICKED:3, DELIVERING:4,
            DONE:5, CANCELED:6, ABNORMAL:7 };
// 合法流转表:from -> [允许的 to]
const TRANS = {
  [S.CREATED]:     [S.ACCEPTED, S.CANCELED],
  [S.ACCEPTED]:    [S.PICKED,   S.CANCELED],
  [S.PICKED]:      [S.DELIVERING, S.ABNORMAL],
  [S.DELIVERING]:  [S.DONE,     S.ABNORMAL],
  [S.ABNORMAL]:    [S.DELIVERING, S.DONE],
};
async function transit(orderId, from, to, detail) {
  if (!TRANS[from] || !TRANS[from].includes(to)) {
    throw new Error(`illegal transit ${from} -> ${to}`);
  }
  const r = await db.query(
    `UPDATE delivery_order SET status = ?, status_detail = ?
      WHERE id = ? AND status = ?`, [to, detail, orderId, from]);
  if (r.affectedRows === 0) {
    throw new Error(`concurrent conflict, order ${orderId} not in ${from}`);
  }
}

条件更新返回 affectedRows = 0 时说明状态已被别人改走,此时应重新读取最新状态再决定后续动作,而不是重试覆盖。状态机的合法流转表单独维护,接口层不写 if-else 硬判断,是这套代码能长期维护的前提。

二、调度模式:抢单与派单的选择

小规模运力(十几个骑手以内)用抢单:新单进入抢单池,骑手在小程序端看到附近订单,点击接单,先到先得。抢单的并发冲突用 Redis 原子操作解决:

async function grab(orderId, riderId) {
  const key = `grab:${orderId}`;
  // SET NX EX:只有一个骑手能抢到
  const ok = await redis.set(key, riderId, 'NX', 'EX', 30);
  if (!ok) throw new Error('order already grabbed');
  try {
    await transit(orderId, S.CREATED, S.ACCEPTED, `rider:${riderId}`);
    await db.query(
      `UPDATE delivery_order SET rider_id = ? WHERE id = ?`, [riderId, orderId]);
  } catch (e) {
    await redis.del(key);   // 状态流转失败,释放抢单锁
    throw e;
  }
}

注意先抢锁再改状态、失败回滚锁的顺序。反过来先改库再抢锁,会出现库里已接单但抢单池里还能抢的窗口。

两种模式的取舍可以用一张表说清楚:

维度

抢单模式

派单模式

适用规模

十几人以内的小运力池

数十人以上的规模运力

实现成本

低, Redis  原子锁即可

高,需要算分与指派服务

骑手自主性

高,自己挑单

低,听系统安排

订单分配均匀度

靠手快,容易扎堆

可按负载均衡

典型问题

好单被抢、差单没人动

骑手对派单不满需要申诉

派单模式适合规模化运力:系统按"距离 + 顺路 + 负载"算分,指派给最合适的骑手。派单算法第一版不必复杂,按 pickup 点到骑手当前坐标的直线距离排序,加上"当前未完成单数"做惩罚项即可。关键是留出改派能力:骑手超时未接、电量和续航出问题,订单要能回到抢单池,这依赖上面状态机里 ACCEPTED -> CANCELED -> CREATED 的流转路径。

三、费用计算:距离分段 + 时段加价

const BASE = 500;            // 起步价 5 元
const STEP = 300;            // 每 300m 一档
const STEP_FEE = 100;        // 每档加 1 元
function calcFee(distMeters, weightKg, hour) {
  let fee = BASE;
  if (distMeters > 1500) fee += Math.ceil((distMeters - 1500) / STEP) * STEP_FEE;
  if (weightKg > 5)      fee += Math.ceil((weightKg - 5) / 3) * 200;
  if (hour >= 11 && hour < 13) fee += 200;   // 午高峰
  if (hour >= 22 || hour < 6)  fee += 300;   // 夜间
  return fee;
}

费用在下单时计算并落库,配送途中不改价;距离用两坐标的 Haversine 公式算,误差对同城场景可接受。运费以分为单位存整数,这个原则在配送系统和支付系统里同样适用。

结算侧也要一并设计。骑手的报酬常见两种模式:按单提成加超时罚扣,或者底薪加单量提成。无论哪种,系统里都要落"应得账":每单的提成、每次异常的责任判定、每次改派的原骑手补偿,逐笔记录,月底结算时和骑手自己对得上。骑手流动性大,账目含糊的运力池留不住人,这是运营层面反复被验证的教训。

四、轨迹上报:节流比精度重要

骑手端 App 或小程序定时上报坐标,看似简单,实际要注意两点。

第一是节流。骑手一单一趟一小时,按 5 秒上报一次要存七百多个点,完全没必要。合理间隔是 30 秒一次,或者位移超过 200 米才上报:

async function reportLocation(riderId, orderId, lat, lng) {
  const key = `loc:last:${riderId}`;
  const last = await redis.hmget(key, 'lat', 'lng', 'ts');
  if (last[2] && Date.now() - Number(last[2]) < 25000) return; // 25s 节流
  await db.query(
    `INSERT INTO rider_track (rider_id, order_id, lat, lng)
     VALUES (?,?,?,?)`, [riderId, orderId, lat, lng]);
  await redis.hmset(key, { lat, lng, ts: Date.now() });
  await redis.expire(key, 3600);
}

第二是用户端展示可以算,不必存。用户查"骑手到哪了",用骑手最后一次坐标和目的地做直线估算即可;真要做 ETA 预估,接地图服务商的路径规划接口,不要自己积累路网数据。

五、超时治理:调度系统真正的日常

上线后会发现,大部分日常问题都是超时:骑手超时未接单、超时未到店、超时未送达。做法是给每个关键节点设阈值,由定时任务扫描:

-- 15 分钟未被接单的运单,回到池顶并通知运营
SELECT id, order_no, created_at FROM delivery_order
 WHERE status = 1 AND created_at < NOW() - INTERVAL 15 MINUTE;

扫描结果先走自动动作(加价重发、通知改派),自动动作无效再转人工。监控面板要按"超时率"而不是"完成单量"考核,完成单量好看不代表体验好。

六、异常件:调度系统真正的日常

正常流程写完只算完成一半,系统好不好用,看的是异常件怎么处理。同城配送最常见的三类异常:收件人联系不上、地址写错、拒收。

联系不上是最常见的。流程上要给骑手明确的操作:到点联系不上,标记"无法联系",系统自动给用户推送通知并开始计时,超过十分钟骑手可以申请返回,运费按规则分担。关键在于让骑手不背锅也不空跑:标记动作带时间戳和定位,后续争议时是凭据。

地址纠错要轻量。用户下单时定位漂移是常态,骑手发现地址不对,在小程序里拍照留言,用户或客服改址后骑手端同步更新。改址要留记录,改过几次、谁改的,结算争议时用得上。

拒收的货物去向要提前定规则:生鲜类当场拍照后按门店指示处理,费用怎么分担写进规则,不要等出了问题再临时协商。异常处理的能力不体现在界面多漂亮,体现在每一种异常都有明确的动作、时限和责任划分。

七、小结

同城配送系统的工程骨架是:状态机条件更新、抢单用 Redis 原子锁、费用下单时锁定、轨迹上报节流、超时靠扫描治理。青海青帝科技在本地配送类项目中,第一版通常只上抢单 + 状态机 + 超时扫描三件套,派单算法等单量到了再迭代——先让系统跑起来,比一开始就做智能调度务实得多。

目录
相关文章
|
1天前
|
云安全 安全 网络安全
|
1天前
|
JavaScript 安全 前端开发
Nodejs Brotli decoder 代码执行漏洞 (CVE-2026-111497)10629
Node.js发布安全更新修复Brotli解码器高危RCE漏洞(CVE-2026-111497),建议受影响版本用户立即升级。
1591 0
|
7天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
1292 27
|
3天前
|
存储 人工智能 自然语言处理
阿里云千问办公官网登录入口:qwenwork.cn网页版免下载,注册送2000积分、登录每日再送100积分
千问办公(QwenWork)是阿里巴巴推出的一站式AI办公平台,定位"不止于对话,更注重交付"。它基于千问大模型,具备任务拆解与多步骤自动执行能力,用户一句话即可完成PPT生成、数据分析、网页搭建、视频剪辑、信息调研等复杂任务,覆盖网页端、桌面端及钉钉生态。网页版免下载,访问qwenwork.cn注册即得2000积分,每日登录再送100积分。个人版免费起用,另有付费版与企业版可选。
|
12天前
|
存储 人工智能 安全
千问办公官网入口网页版登录:qwenwork.cn 注册送2000积分、登录送100积分
阿里云千问办公(QwenWork)是AI智能工作平台,支持一句话生成PPT、表格、网页、视频及数据分析。个人免费版送2000积分+每日登录赠100分,含1GB存储、5个发布页;企业版198元/席/月起。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
7天前
|
人工智能 开发者
Qoder CN 大使计划正式开启|无限创造,等你一起
全年免费积分、优先内测、活动支持与更多共创机会,邀请用 Qoder 把想法变成现实的你。
609 6
|
9天前
|
安全 Linux 芯片
【万字长文】Ollama下载、安装、配置、使用保姆级教程(2026最新图解)
Ollama 是一款免费开源的本地大模型运行工具,支持 DeepSeek、Qwen 等主流模型,一条命令即可下载运行。2026 最新版 v0.35.1 官方安装包(1.47GB)已提供国内直连下载,纯本地、离线可用、无广告、无隐私泄露风险。
834 2
|
1月前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
10102 33
|
1天前
|
安全 应用服务中间件 Go
PHP2Shell 漏洞拆解:WordPress 与 nginx 为何中招
PHP2Shell 双 RCE 已遭在野利用:WordPress、Drupal 全在射程内,nginx 其实另有洞。拆成因与修复顺序
PHP2Shell 漏洞拆解:WordPress 与 nginx 为何中招
|
9天前
|
Web App开发 人工智能 JavaScript
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
REA(Reverse Engineer Anything)是近期爆火的开源AI逆向工具,4月发布,1个月内斩获15万+ Star。它让AI Agent无需源码即可分析二进制、APK、JS/Electron、HAR包等17类目标,自动反编译、生成可运行代码,并强制返回带证据与局限的结论。全本地执行,MIT协议,支持Hopper/Ghidra/IDA等引擎,一行命令即可上手。
841 2
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA