☰
代驾小程序核心开发:状态机、实时定位与抢单并发控制实战
2026/10/6 10:01:39 网站建设 项目流程

先说个结论:代驾小程序和普通电商小程序完全是两个物种。电商小程序的核心是商品和订单,代驾小程序的核心只有一件事——状态。从哪里出发、司机在哪、谁接的单、车开到哪了、钱怎么算,全部围绕订单状态在转。我在做代驾小程序开发实战时,把大部分精力都花在了状态机、实时位置和并发控制上,页面反而是最后才写的。这篇文章就把核心代码实现拿出来拆一遍,重点讲清楚:用户端怎么下单、司机端怎么抢单、轨迹怎么存、钱怎么算,以及我在线上踩过的那些坑。

适合谁看?准备做代驾、跑腿、同城即时配送类小程序的技术同学,或者已经在做但想优化核心链路的人。你不需要懂特别深的地图算法,但最好对微信小程序开发、redis 基础操作有一定了解。下面所有代码都是我自己项目里精简后的版本,可以直接拿去改。

1. 代驾业务的状态机:代码里所有“会变”的字段都围着它转

1.1 状态定义与流转边界

很多新手上来就写接口,先写一个 createOrder,再写一个 cancelOrder,写着写着就乱了。原因是他们没意识到,代驾订单的每个状态变更都牵扯到用户端、司机端、后台三方的同步。我第一步就是把状态机定死,所有接口只允许做“从状态 A 到状态 B”的迁移,其他操作全部拒绝。

我常用的订单状态定义如下:

// orderStatus.js const ORDER_STATUS = { // 用户已下单,等待司机抢单 WAITING_DRIVER: 10, // 司机已抢单,正在前往用户起点 DRIVER_ON_THE_WAY: 20, // 司机已到达起点,等待用户上车 DRIVER_ARRIVED: 30, // 代驾中,车辆正在行驶 IN_SERVICE: 40, // 司机已到达目的地,待用户确认 ARRIVED_DEST: 50, // 用户确认,待支付 PENDING_PAYMENT: 60, // 已支付,订单完成 COMPLETED: 70, // 用户取消 USER_CANCELLED: 0, // 司机取消 DRIVER_CANCELLED: -1, // 系统超时取消 TIMEOUT_CANCELLED: -2, };

状态对应的操作语义:

当前状态允许的操作目标状态说明
WAITING_DRIVER用户取消USER_CANCELLED未接单前用户随时可取消
WAITING_DRIVER司机抢单DRIVER_ON_THE_WAY抢单后锁定司机
DRIVER_ON_THE_WAY司机确认到达DRIVER_ARRIVED到达起点后通知用户
DRIVER_ON_THE_WAY用户取消USER_CANCELLED司机还没到,用户可取消
DRIVER_ARRIVED开始服务IN_SERVICE用户上车,司机点击“开始代驾”
IN_SERVICE到达目的地ARRIVED_DEST司机点击结束服务
ARRIVED_DEST用户确认支付PENDING_PAYMENT生成支付单
PENDING_PAYMENT支付成功COMPLETED支付回调触发

这里有个特别容易忽略的点:司机抢单后到司机到达之前,用户取消订单是有代价的。不同于网约车,代驾司机已经动身去你的位置了,所以这段取消只允许在“司机尚未出发太久”或“系统判责”的情况下通过。我的做法是在状态机之外加了取消原因和判责逻辑,而不是直接允许无条件迁移。

1.2 服务端状态流转校验

单有状态定义不够,容易在代码层面被绕过。比如用户端直接调了“开始服务”,这在业务上是不允许的。所以我所有状态变更都统一走一个入口:

// orderStateMachine.js const stateMachine = { [ORDER_STATUS.WAITING_DRIVER]: { userCancel: [ORDER_STATUS.USER_CANCELLED, 'canCancelByUser'], driverGrab: [ORDER_STATUS.DRIVER_ON_THE_WAY, 'canGrabByDriver'], timeoutCancel: [ORDER_STATUS.TIMEOUT_CANCELLED, 'canTimeout'], }, [ORDER_STATUS.DRIVER_ON_THE_WAY]: { userCancel: [ORDER_STATUS.USER_CANCELLED, 'canCancelBeforeArrive'], driverArrive: [ORDER_STATUS.DRIVER_ARRIVED, 'canConfirmArrive'], driverCancel: [ORDER_STATUS.DRIVER_CANCELLED, 'canCancelByDriver'], }, [ORDER_STATUS.DRIVER_ARRIVED]: { startService: [ORDER_STATUS.IN_SERVICE, 'canStartService'], }, [ORDER_STATUS.IN_SERVICE]: { endService: [ORDER_STATUS.ARRIVED_DEST, 'canEndService'], }, [ORDER_STATUS.ARRIVED_DEST]: { confirmPay: [ORDER_STATUS.PENDING_PAYMENT, 'canCreatePayment'], }, [ORDER_STATUS.PENDING_PAYMENT]: { paySuccess: [ORDER_STATUS.COMPLETED, 'canPaySuccess'], }, }; function transition(order, action, operator) { const rule = stateMachine[order.status]?.[action]; if (!rule) throw new Error('非法操作:当前状态不支持该动作'); const checker = rule[1]; // 业务校验函数各自实现 if (!businessChecks[checker]?.(order, operator)) { throw new Error('业务校验未通过:' + checker); } order.status = rule[0]; return order; }

这样做的好处是,所有状态变更逻辑集中在一处,排查问题的时候打开这一个文件就知道某个状态能不能做某个动作,不需要翻遍整个服务端代码。实际开发里我还加了状态变更流水表order_status_log,每次迁移都插入一条记录,线上出纠纷时直接以流水为准。

2. 用户端叫车链路:定位、下单、等待,三个环节的代码实现

2.1 定位授权与逆地址解析

小程序拿位置很简单,wx.getLocation一行代码,但麻烦的是权限和精度。小程序必须用户在“设置”里授权“位置信息”,而且 2022 年以后微信还加了“精确位置”的开关,用户不开精确位置,你拿到的坐标可能偏差几百米,对代驾这种需要司机精准找车的场景几乎是致命的。

我的做法是三步:

  1. 进首页先调wx.getSetting检查授权状态;
  2. 未授权就弹引导,用户拒绝授权就提示去设置页打开;
  3. 拿到坐标后,用微信自带wx.reverseGeocoder或者服务端逆地址解析,把经纬度转成可读地址。
// 定位模块封装 function ensureLocation() { return new Promise((resolve, reject) => { wx.getSetting({ success: (res) => { const auth = res.authSetting['scope.userLocation']; if (auth === false) { wx.showModal({ title: '需要定位权限', content: '请在小程序设置中开启位置权限', confirmText: '去设置', success: (r) => { if (r.confirm) wx.openSetting(); }, }); reject(new Error('no location auth')); return; } wx.getLocation({ type: 'gcj02', isHighAccuracy: true, highAccuracyExpireTime: 3000, success: resolve, fail: reject, }); }, fail: reject, }); }); }

这里有一个关键细节:微信返回的是 gcj02 坐标,不是纯 GPS 坐标。如果你的后端和地图服务用的是其他坐标系,一定要做坐标转换。我因为没注意这个,第一版上线后司机导航偏离了几百米,后来客户端统一用 gcj02,后端所有点位也约定 gcj02,才彻底解决。

2.2 创建订单的幂等策略

用户点“立即叫代驾”按钮,最怕什么?两次请求生成了两笔订单。小程序网络抖动、用户连点、前端重试,都会导致重复下单。

我在订单表加了一个idempotent_key字段,用户端在下单前生成一个一次性 UUID,写入按钮状态里。后端收到订单请求,先查这个 key,存在就直接返回旧订单,不存在才创建。

// 用户端点击下单 async function submitOrder() { if (this.submitting) return; this.submitting = true; const idempotentKey = `${user.id}_${Date.now()}_${Math.random().toString(36).slice(2)}`; try { const res = await request('/order/create', { startAddr: this.startAddr, endAddr: this.endAddr, startLat: this.startLat, startLng: this.startLng, endLat: this.endLat, endLng: this.endLng, carNumber: this.carNumber, carBrand: this.carBrand, idempotentKey, }); if (res.code === 0) { this.orderId = res.data.orderId; // 跳转等待页 } } finally { this.submitting = false; } }
// 服务端创建订单 async function createOrder(user, payload) { const exist = await OrderModel.findByKey(payload.idempotentKey); if (exist) { return { code: 0, data: { orderId: exist.id, repeat: true } }; } const distance = calcDistance(payload.startLat, payload.startLng, payload.endLat, payload.endLng); const order = await OrderModel.create({ userId: user.id, ...payload, distance, status: ORDER_STATUS.WAITING_DRIVER, createTime: Date.now(), }); // 推送到司机侧抢单池 await pushToDriverPool(order); return { code: 0, data: { orderId: order.id, repeat: false } }; }

顺带说一句,pushToDriverPool这里我用了 redis 的有序集合,以经纬度计算后的 geohash 为分区,把订单推给距离起点 3 公里内的司机。这个小细节后面第 3 节专门讲。

2.3 等司机接单:轮询还是 WebSocket

下单之后用户端要实时看到“司机正在赶来”,这就有个消息通道的问题。微信小程序里,原生 WebSocket 复杂度略高,但毕竟订单状态不像股票行情那么高频,我采用的是“WebSocket 推送 + 兜底轮询”双通道。

主链路用 WebSocket:

// socket.js 简化版 function connectSocket(token) { const ws = wx.connectSocket({ url: `wss://api.example.com/ws?token=${token}` }); ws.onMessage((event) => { const msg = JSON.parse(event.data); if (msg.type === 'order_status') { // 通知页面更新订单状态 emit('order_status', msg.data); } if (msg.type === 'driver_grab') { // 司机抢单成功,立刻展示司机信息 emit('driver_grab', msg.data); } }); ws.onClose(() => { // 断线重连,指数退避 setTimeout(() => connectSocket(token), 3000 * Math.pow(1.6, retryCount)); }); }

WebSocket 断线在小程序里特别常见,杀后台、切网络、长时间挂起都会被系统断掉。所以我在订单详情页同时加了 5 秒一次的轻量轮询,只在“等待接单”和“司机前往中”两个状态开启。一旦 WebSocket 先推送了状态,轮询会自动停掉。

3. 司机端接单链路:GPS上报、抢单防重、服务状态推进

3.1 司机位置实时上报

司机端是整个代驾小程序里最吃性能的端。司机开着自己的车,手机一直跟着跑,不仅要上报位置,还要过滤掉定位漂移。

先说上报频率。我最后调成 3 秒一次,太快费流量费电,太慢抢单围栏判断不准。上报接口是一个独立的轻量接口,不走业务鉴权的主链路,单独用一张driver_location_log表记录,实时位置则直接写 Redis GEO。

// 司机端位置上报:服务端接收 async function reportDriverLocation(driverId, lat, lng, speed, heading) { const key = `driver:loc:${driverId}`; // 写入实时位置 await redis.geoAdd(key, { longitude: lng, latitude: lat, member: `${lat},${lng}` }); // 设置 10 分钟过期,防止司机离线后残留脏数据 await redis.expire(key, 600); // 不入库,只往 Kafka 里丢一条,由消费端异步写日志 await kafka.send('driver-location-log', { driverId, lat, lng, speed, heading, ts: Date.now(), }); }

这里为什么入 Redis 而不直接写 MySQL?因为高频写入直接打崩数据库。司机位置是“只读当前值”的数据,Redis 的 GEO 类型天然适合,而且后面查附近司机可以直接用georadius,比自己算距离再排序快一个数量级。

3.2 基于 Redis 锁的抢单实现

抢单是这个项目里并发最集中的场景。一个订单推给 20 个司机,20 个人同时点“抢单”,数据库层如果不加控制,很可能两个人都抢成功。

我的实现是 redisSET NX EX,拿到锁的司机才能进入后续业务校验:

// 抢单接口核心逻辑 async function grabOrder(driverId, orderId) { if (!driverId || !orderId) throw new Error('参数错误'); const lockKey = `lock:order:${orderId}`; // 抢锁,5 秒自动过期 const locked = await redis.set(lockKey, driverId, 'NX', 'EX', 5); if (!locked) { return { code: 4001, msg: '手慢了,订单已被别的师傅抢走' }; } try { const order = await OrderModel.findById(orderId); // 二次校验状态,防止锁过期后状态已经变化 if (order.status !== ORDER_STATUS.WAITING_DRIVER) { return { code: 4002, msg: '订单状态已变化' }; } // 更新订单状态 + 司机ID,事务保护 await OrderModel.transaction(async (tx) => { await tx('orders').where({ id: orderId }).update({ status: ORDER_STATUS.DRIVER_ON_THE_WAY, driver_id: driverId, grab_time: Date.now(), }); }); // 推送通知用户 await pushToUser(order.userId, 'driver_grab', { orderId, driverId, carNumber: '沪A12345', driverName: '王师傅', }); return { code: 0, msg: '抢单成功' }; } finally { // 抢单成功后锁删除;但这里要考虑业务操作耗时超过锁过期时间的问题 const owner = await redis.get(lockKey); if (owner === String(driverId)) { await redis.del(lockKey); } } }

抢单有个实战坑必须提醒:锁的过期时间一定要长于业务操作耗时。最开始我设了 2 秒,结果有次数据库慢查询,业务逻辑跑了 3 秒,锁自动过期了,第二个司机又把锁拿走了,等第一个司机事务提交完,订单状态已经变成第二个司机的了。后来我把锁过期时间设为 5 秒,并在释放锁之前先比对持有者 ID,只删自己的锁,避免误删别人的锁。

3.3 司机端的状态推进

司机端和用户端操作的是同一笔订单,但视角不同。司机端页面里有三个核心动作:到达起点、开始代驾、结束代驾。这三个动作全部走第 1 节那个状态机入口。

一个需要特别注意的业务点是“开始代驾”和“结束代驾”与用户确认的关系。我的逻辑是:

  • 司机点“开始代驾”,相当于宣告服务开始,后续计费从这一秒算;
  • 司机点“结束代驾”,订单进入ARRIVED_DEST,此时用户端弹窗显示里程、时长、费用,用户点“确认并支付”后状态才切到PENDING_PAYMENT。

如果司机点完“结束代驾”,用户一直不确认呢?这里必须有个兜底:用户超过 5 分钟不确认,系统自动把订单置为PENDING_PAYMENT,并给用户发提醒。否则司机的订单永远卡在已到达状态,钱收不回来。

// 结束代驾 async function endService(orderId, driverId) { const order = await OrderModel.findById(orderId); if (order.driverId !== driverId) throw new Error('无权操作'); const updated = transition(order, 'endService', { id: driverId }); await OrderModel.updateStatus(order.id, updated.status); // 计算费用并写入订单金额 const fee = await calculateFee(order); await OrderModel.updateFee(order.id, fee); // 发送到用户端确认 await pushToUser(order.userId, 'end_service', { orderId, fee }); // 设置 5 分钟自动确认定时任务 await scheduleAutoConfirm(order.id, 5 * 60 * 1000); return { code: 0, data: { fee } }; }

这里我用了scheduleAutoConfirm,底层是 redis 延迟队列。订单 ID 作为 task,5 分钟后消费端检查订单状态是否还在ARRIVED_DEST,如果是就自动推进到PENDING_PAYMENT。定时任务不是 setInterval 扫表,那种做法数据量大了会漏,延迟队列才靠谱。

4. 轨迹回放与动态计费:核心代码里的两个硬骨头

4.1 轨迹点采集与存储

代驾服务中,用户最关心的就是轨迹回放,司机也怕用户事后扯皮说“绕路了”。所以服务开始后,司机端以 5 秒一个点的频率上传位置,服务端只保留服务区间内的点。

轨迹点上传接口设计的核心是批量。司机端每 15 秒把 3 个点聚合成一个数组,一起传上来。理论上 30 分钟的代驾大概产生 360 个点,全天几千单就是百万量级,全量查 MySQL 会扛不住。我的方案是:

  • 轨迹点一张独立表driver_track_log,按订单 ID 索引;
  • 同时每个订单的轨迹点按 5 分钟一个 chunk 写入 Redis,用于实时回放;
  • 历史轨迹回放从 MySQL 读,实时轨迹回放从 Redis 读。
// 轨迹批量上报 async function uploadTrack(orderId, driverId, points) { const order = await OrderModel.findById(orderId); if (!order || order.driverId !== driverId) throw new Error('非法上报'); const rows = points.map((p) => ({ order_id: orderId, lat: p.lat, lng: p.lng, speed: p.speed || 0, ts: p.ts, })); await TrackModel.batchInsert(rows); // 同时写 Redis:轨迹点分 60 秒块存储 const chunkKey = `track:${orderId}:${Math.floor(rows[0].ts / 60000)}`; await redis.rpush(chunkKey, rows.map((r) => JSON.stringify(r))); await redis.expire(chunkKey, 2 * 3600); }

轨迹点的存储上,我强烈建议加上ts时间戳索引,因为后面做里程回溯、绕路判断都要按时间排序。

4.2 距离计算与计费引擎

代驾计费不是简单的“每公里多少钱”。我当时做的计费规则有四个维度:起步价、里程费、等待费、夜间附加费,其中里程费是按实际轨迹计算的,不是直线距离。

所以calculateFee的逻辑是:

  1. 从轨迹点表读出全部点;
  2. 用 Haversine 公式计算相邻两个轨迹点的球面距离,累加出实际代驾里程;
  3. 过滤漂移点(速度大于 120km/h 或相邻点位移突变超过 500 米);
  4. 根据里程阶梯计算里程费;
  5. 加上起步价、等待费(司机到达起点后等待超过 10 分钟的部分按分钟计费)、夜间附加费。
// 计算两点球面距离,Haversine 公式 function haversine(lat1, lng1, lat2, lng2) { const R = 6371000; // 地球半径,米 const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLng = ((lng2 - lng1) * Math.PI) / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }
// 订单里程计算 + 漂移点过滤 async function calcOrderMileage(orderId) { const points = await TrackModel.getByOrderId(orderId); if (points.length < 2) return 0; let total = 0; for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const cur = points[i]; const dist = haversine(prev.lat, prev.lng, cur.lat, cur.lng); const timeDiff = (cur.ts - prev.ts) / 1000; // 秒 // 漂移点过滤:速度超过 120km/h(33.3m/s) 或 相邻点位移大于 500m,认为是漂移 if (dist > 500 || timeDiff > 0 && dist / timeDiff > 33.3) { continue; } total += dist; } return Math.round(total); // 返回米 }

计费引擎的核心原则是:费率和规则尽量配置化,不要在代码里写死。我第一版图省事,直接把里程单价写死在calculateFee里,后来改价时前端已经发版,后端又得重新部署,非常痛苦。后来我把起步价、里程单价、等待单价、夜间附加费全部改成配置中心管理,改价就是改一条配置,系统自动生效。

这里也提一个容易被坑的点:计费要以服务区间为界。司机开着代驾用户的车,从起点到终点的轨迹就算计费里程,但司机自己开车过来接用户的里程绝对不能算进去。所以我在startService时记录了一个service_start_time,轨迹查询只取ts >= service_start_time的数据,以免算错。

4.3 轨迹回放接口设计

轨迹回放接口要注意性能,因为一次请求要返回几百个点,点过多前端渲染会卡。我做了降采样:轨迹点超过 200 个时,按等间隔抽稀到 200 个点,既能看清路线路径,又不至于卡死页面。

// 轨迹回放接口 async function getTrackForReplay(orderId) { const points = await TrackModel.getByOrderId(orderId); if (points.length <= 200) return points; const step = Math.ceil(points.length / 200); const sampled = []; for (let i = 0; i < points.length; i += step) { sampled.push(points[i]); } // 同时保证最后一个点一定在轨迹里 sampled[points.length - 1] = points[points.length - 1]; return sampled; }
// 用户端渲染回放(简化) function drawTrack(canvas, points) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); points.forEach((p, i) => { const { x, y } = projectToScreen(p.lat, p.lng); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); }

前端回放用的projectToScreen是把经纬度投影到平面坐标,我用的是简单的等距圆柱投影加上屏幕适配,够用即可,不需要引入高精度地图投影。当然小程序里更常见的方案是直接在地图组件map的polyline上渲染折线,我贴坐标图只是示意。

5. 线上踩过的坑:状态不一致、定位漂移、支付回调延迟

5.1 状态不一致的复现场景

状态不一致是代驾系统最大的坑,没有之一。最常见的场景是:司机端显示“代驾中”,用户端显示“已结束”,后台显示“待支付”,三个端各说各话。原因往往是端上异常操作绕过状态机直接改了本地状态,或者服务端接口没有做幂等导致重复提交。

我的处理方案有三层:

  1. 服务端所有状态变更必须走状态机,不允许接口内部直接update status;
  2. 每次状态变更都写order_status_log流水,记录操作人、操作时间、旧状态、新状态、操作来源(用户端/司机端/服务端);
  3. 端上不做“乐观状态”,统一以服务端推送为准。司机端、用户端收到推送后刷新页面状态,本地不缓存状态用来做界面判断。

5.2 定位漂移怎么过滤

代驾需求对定位精度要求极高,但现实中定位漂移不可避免。有一次线上反馈:司机明明在路上,轨迹却显示绕进了旁边的湖里。

我总结了几种常见的漂移形态:

  • 高楼密集区 GPS 信号反射,点位在道路两边横跳;
  • 地下车库/隧道没有 GPS,定位长时间保持不变或突然跳到远处;
  • 手机省电模式关闭 GPS,只靠基站粗略定位。

过滤策略我做了两层。第一层是司机端上报前过滤:

// 司机端上报前过滤漂移 function filterPoint(prev, cur) { if (!prev) return true; const dist = haversine(prev.lat, prev.lng, cur.lat, cur.lng); const timeDiff = (cur.ts - prev.ts) / 1000; // 平均速度超过 40m/s(144km/h) 或 位移 > 300m,认为漂移 if (timeDiff > 0 && dist / timeDiff > 40) return false; if (dist > 300) return false; return true; }

第二层是服务端入库前过滤,两套规则独立。即使客户端被人篡改或者异常上报,服务端也能挡掉坏数据。过滤掉了不等于丢掉,真正的订单轨迹里如果有漂移点,在计费时不参与里程累加,但在回放里保留用于查看上下文。

5.3 支付回调延迟与订单超时

最后一个坑:微信支付回调不是即时到达的,用户在支付成功后可能要等一两秒甚至更久才能收到回调。如果用户端直接以“支付成功”的页面文案为准,而服务端订单还是PENDING_PAYMENT,用户就以为支付失败了,重复付款的投诉就来了。

我的处理方案是:用户端支付成功跳转前,先调一个“查询支付状态”接口,服务端主动向微信侧查询支付结果,确认完成后才把订单置为COMPLETED。同时后端维护一个延迟队列,支付单创建后 15 分钟如果还没有回调,主动向微信查询一次,兜底完成状态推进。

// 服务端主动查询支付结果 async function queryPayAndSettle(orderId) { const order = await OrderModel.findById(orderId); if (order.status !== ORDER_STATUS.PENDING_PAYMENT) return; const result = await WechatPay.query({ outTradeNo: order.payTradeNo, }); if (result.trade_state === 'SUCCESS') { const updated = transition(order, 'paySuccess', { id: 0, source: 'pay-query' }); await OrderModel.updateStatus(order.id, updated.status); await pushToUser(order.userId, 'pay_success', { orderId }); } }

这套主动查询逻辑救了我很多次,因为晚高峰支付量大的时候,微信回调偶尔会延迟,靠轮询兜底才没让用户端和服务端的状态脱节。

再说个超时自动取消的实现,也踩过坑。订单在WAITING_DRIVER状态下,3 分钟内没司机抢单就自动取消。我当时用setTimeout在进程内存里调,结果服务一重启,定时任务全部丢失。后来改成 redis 延迟队列,zadd一个未来的时间戳,消费端循环zrangebyscore取出到期任务处理,服务重启也不会丢。

// 延迟队列:订单超时取消 async function scheduleAutoCancel(orderId, ttlMs) { const score = Date.now() + ttlMs; await redis.zadd('delay:cancel-order', score, String(orderId)); } // 消费端伪代码 setInterval(async () => { const now = Date.now(); const expired = await redis.zrangebyscore('delay:cancel-order', 0, now); for (const id of expired) { await cancelOrderIfWaiting(id); await redis.zrem('delay:cancel-order', id); } }, 1000);

第一次用内存 setTimeout 时,线上一次重启导致几十个待接单订单全部卡死,用户骂声一片。换到 redis 延迟队列之后,这个问题再没出现过。

我在实际做代驾项目的最大体会是:宁可页面丑一点,也不能让状态乱一点。代驾小程序的核心不在 UI 多炫,而是订单状态流转的可靠性、实时位置的准确性、费用计算的严谨性,这三块代码搞扎实了,线上就不会出大问题。如果你也是刚起步做类似项目,先把状态机、抢单锁、延迟队列这三样东西吃透,后面扩展功能都会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询