拿到这个项目的时候,我第一时间想的是:市面上那么多跑腿小程序,真正能把订单调度、配送路径和支付核销讲清楚的开源实现确实不多。大多数能搜到的“源码”无非是把登录、下单、地图展示这些壳子搭出来,一碰到骑手抢单的并发、支付回调的幂等、坐标偏移这种生产环境必经的坎,就没了下文。
所以这篇博文既不贴一大段完整工程代码,也不会只讲概念。我按这套Java同城跑腿小程序自己做过的完整落地路径来写——从业务建模、订单调度策略,到地图配送路线,再到支付核销的异常处理,每一步都给出了我在实际项目中验证过的实现思路与核心代码片段。如果你正准备自己搞一套跑腿系统,或者想用这类项目作为Java后端练手,这篇内容应该能帮你少踩几个坑。
1. 跑腿小程序的项目轮廓:模块边界一眼看穿
1.1 跑腿订单核心流程
同城跑腿和普通外卖有一个本质区别:外卖是“商家→用户”,跑腿是“任意取件点→任意送件点”。这个区别直接决定了订单调度、路线规划和支付核销的复杂程度都上升了一个量级。先明确一下我这套系统的业务范围:用户下单跑腿时,要分别填写取件地址、送件地址、物品类型、期望送达时间、小费金额等信息;系统接单后骑手会抢单或者由后台派单,骑手到取件点揽收,中途可以上报状态,最终送货上门后由用户输入核销码完成订单,同时发起支付结算。
模块划分上,我按照这个链路拆成了几个核心服务:
- 用户端小程序(下单入口、订单查询、支付、核销)
- 骑手端小程序(抢单大厅、待取件列表、配送导航、状态上报)
- 管理后台 Web(订单查询、骑手管理、费率配置)
- Java 后端服务(用户、订单、调度、支付、消息推送)
考虑到大部分同城业务团队起步时不会直接上微服务,我整套代码初版用了单体的 Spring Boot 架构,但通过包结构把订单、支付、调度、用户这些领域边界划得清清楚楚,后面真要拆服务时按包直接拆就行。技术栈上,Spring Boot 2.7 + MyBatis-Plus + Redis + MySQL + RabbbitMQ,小程序端原生或 uni-app 都可以对接。这个组合成熟度够高,招人容易,出问题网上一搜一大把现成答案。
1.2 数据库模型的几个关键表
跑腿业务核心表我个人总结就是六张:用户表、骑手表、订单主表、订单状态流水表、支付单表、账户流水表。
订单主表是业务中最重要的表,字段设计上有些地方必须提前想清楚:
- order_no 业务订单号,用户可读,例如 F 开头或日期+随机数
- order_type(取件送件、同城购买、代排队等等)
- sender_name / sender_phone / sender_address / sender_lng / sender_lat
- receiver_name / receiver_phone / receiver_address / receiver_lng / receiver_lat
- goods_type、goods_weight、goods_value(这是一个极容易忽略的字段,贵重物品理赔时全靠它)
- distance(预估配送距离)
- coupon_amount / tip_amount / total_amount
- status 订单状态
状态流水表是个容易被新手忽略的重磅表。订单状态变化的每一次操作都要记录下来,后台客服在处理纠纷时全靠它还原现场。我在代码里强制要求所有状态变更必须调用统一的 statusChange 方法写流水,严禁业务代码直接 update 订单状态字段。
支付单表和订单主表为什么分开?因为业务上存在一个订单多次补差价、多个订单合并支付等复杂场景。拆开后每次支付操作独立对应一个支付单记录,对账也简单。
1.3 订单状态机怎么设计才能不乱
状态机的设计直接决定后续所有业务逻辑的复杂度。跑腿订单的状态我用的是一套相对标准但不臃肿的链路:
text复制WAIT_ACCEPT(待接单)
-> ACCEPTED(已接单)
-> PICKED_UP(已取件)
-> DELIVERING(配送中)
-> COMPLETED(已完成)
-> CANCELLED(已取消,终态)
特殊分支还包括:待接单超时自动取消、骑手到店后用户取消产生取消费、配送异常上报后后台介入等。每一笔状态变更都需要后端做前置校验,不允许从 PICKED_UP 直接跳 CANCELLED,必须先通过申诉流程转成后台强制取消。
我接触过一些做跑腿的创业团队,开发贪图方便,前端点一下就改一个状态,完全不走后端校验。等单量上来后客服后台一查全是状态打架的记录,一个订单既显示已完成又显示退款中,资损和投诉一起爆发。所以从第一版开始就别省状态机的力气,后面你会感谢这个决定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 订单调度引擎:距离、并发与抢单公平性的实战取舍
2.1 订单调度到底“调”的是什么
同城跑腿调度系统的目标就一句话:用最短的时间和合理的成本,找到合适的骑手完成配送。而“合适”这个词,在不同模式下计算方法完全不同。
我这次做的是主流且最容易启动的小程序场景:抢单模式,配合后台手动派单。系统把用户下单产生的订单放入“订单池”,然后根据地理范围推送给附近的骑手;骑手端看到的是一个单子列表,谁手快谁抢。派单模式则需要一套更复杂的运筹算法,适合有稳定运力池的团队,第一版不建议碰。
整个调度过程核心要处理三个问题:
- 订单推给谁?—— 根据骑手当前位置与取件点距离做圈选
- 并发抢怎么保证只有一个骑手中单?—— 数据库行锁 + Redis 预占
- 超时没骑手抢怎么办?—— 超时转派、加小费重新广播、后台手动派
2.2 经纬度距离计算不是调个公式就行
同城业务中距离计算是最底层的依赖。骑手和订单距离多少?取件地和送件地距离多少?配送费怎么定?这些都需要快速、准确地算距离。
我第一版直接用的 Redis GEO 来算骑手与订单的距离和圈选附近骑手。Redis 3.2 之后自带了 GEO 类型,基于 sorted set 实现,指令简单,georadius 一下就能拿到附近骑手。但有一个坑是精度问题,Redis GEO 的底层是 GeoHash 编码,精度在米级以内,做“附近3公里有单”这种筛选完全足够,但要精确计算骑手到取件点的实际骑行距离就不够用了。
骑行距离与直线距离差距巨大,尤其国内城市道路不规则、高架桥多。如果配送费完全按直线距离算,骑手跑一单远路却拿少了,运力马上流失。
最后我采用的是“分层距离策略”:
- 骑手与订单距离圈选用 Redis GEO 粗筛
- 配送费计算与线路展示走地图服务商的骑行路线规划接口
- 直线距离作为兜底与超区判断
2.3 并发抢单如何避免“手快有手慢无”的资损级问题
高并发抢单是跑腿系统最刺激的场景。两个骑手同时看到同一个大单,几乎同时点了抢单,如果代码没有做好并发控制,就会出现两个人都抢成功的“超卖”现象。第一版图省事用了先查后更的写法:
java复制// 错误示例:多线程下一定会出问题
if (order.getStatus().equals(WAIT_ACCEPT)) {
order.setStatus(ACCEPTED);
order.setRiderId(riderId);
orderMapper.updateById(order);
}
这个写法在并发量稍微上来一点后必出事故。两个请求同时读到 WAIT_ACCEPT,同时执行更新,后更新的把先更新的覆盖了。表面看占位成功,实际数据库里同一个订单的 rider_id 字段被写成了两个人,后面骑手 A 和骑手 B 都去商家取货,直接冲突。
正确写法分两层:
第一层,Redis 预占。抢单请求先执行 setIfAbsent 把 orderId 锁住,只有成功拿到锁的骑手才允许进入后续流程。第二层,数据库乐观锁兜底。更新时带上条件 status = WAIT_ACCEPT,影响行数为 0 说明已经被抢走。
java复制// Redis 层预占
Boolean acquired = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:order:accept:" + orderId, riderId, 10, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(acquired)) {
throw new BizException("手慢了,订单已被抢走");
}
// 数据库乐观锁兜底
int rows = orderMapper.acceptOrder(orderId, riderId, WAIT_ACCEPT);
if (rows == 0) {
stringRedisTemplate.delete("lock:order:accept:" + orderId);
throw new BizException("手慢了,订单已被抢走");
}
这条链路验证过高峰期几千人同时抢几十个订单也不会产生一单多骑手的问题,值得抄走。
2.4 超时无人接单的梯度处理策略
单量稀疏的非核心城区,或者凌晨时段,经常遇到订单挂出去五分钟没人抢。用户那边会不断催促,体验极差。
我的处理方案是定时任务扫描加事件驱动结合。订单超过 N 分钟未接单,触发一次“优先级提升”动作:
- 第一次超时(比如3分钟),扩大推送半径,从3公里推送到5公里
- 第二次超时(比如6分钟),系统自动增加“调度奖励金”,金额从订单服务费里出
- 第三次超时(比如10分钟),进入人工干预列表,后台管理人员可以直接指派给附近签到的骑手
这个阶梯策略在代码实现中建议用 delayed queue 或者 RocketMQ/RabbitMQ 的延迟消息,而不是每分钟扫一次全表。我当时用了 RabbitMQ 延迟插件,每笔订单创建时按需投递几条延迟消息,时间到了触发对应方法查一下订单状态,如果已经被抢,直接丢弃消息不处理,逻辑干净且对数据库压力小。
3. 配送路线规划与地图能力的正确打开方式
3.1 地图服务商选型:避免被 API 费用和配额绑架
配送路线这块不是说你在小程序里嵌入个地图组件、显示个 marker 就完了。跑腿业务真正要的地图能力有三个:路线规划、逆地理编码、实时定位。
服务商上我建议国内业务优先在高德、腾讯、百度之间三选一。各家对比下来个人经验如下:
| 维度 | 高德 | 腾讯 | 百度 |
|---|---|---|---|
| 路线规划接口成熟度 | 最高 | 高 | 高 |
| 与微信小程序原生兼容度 | 好用 | 核心匹配 | 一般 |
| 坐标系 | GCJ-02 | GCJ-02 | GCJ-02 |
| 个人开发者免费配额 | 有,需要申请 | 有,小程序场景亲和 | 有 |
| 骑手App导航接入成本 | 低 | 低 | 中 |
选用腾讯地图还有一个天然好处:其 SDK 对微信小程序和微信生态的支持是最顺滑的,官方提供了微信小程序专用的 JavaScriptSDK,直接调用起路由规划来很省事。如果后端使用的高德,小程序端展示坐标的时候仍要统一在 GCJ-02 坐标系下处理。
3.2 坐标偏移是你迟早要踩的坑
国内地图坐标系和其他地区不一样,需要特别注意 GCJ-02 加密偏移。
有一次测试反馈骑手明明在用户楼下,地图上显示的骑手定位点却偏出去一百多米,后台判断骑手没有到店,一直不能点“已取件”,骑手被卡得破口大骂。排查到最后发现:小程序端 wx.getLocation 返回的是 GPS 原始坐标(WGS-84),而腾讯/高德地图底图和逆地理编码要求 GCJ-02。GPS 坐标在中国大陆地区展示时没有纠偏,底图却已经是加密过的,自然就出现了一百米的偏差。
解决方案是前端调用 wx.getLocation 时加一个参数:
javascript复制wx.getLocation({
type: 'gcj02',
success(res) {
// 拿到的 res.latitude / res.longitude 已经是 GCJ-02
}
})
所有上报给后端的坐标统一使用 GCJ-02,后端在做距离判断、围栏判断时都用同一条坐标标准。前端千万不要拿 WGS-84 直接调后端接口,后端也别自作聪明去做火星坐标转换,全链路统一坐标系才不会再出诡异问题。
3.3 算距离的三种方式与费用联动
距离计算是配送定价的核心依据。我在实际项目里详细对比了三种方案,建议根据场景分开用:
- 直线距离:
Math.haversine公式或者 Redis GEO dist,性能极高,适合做超区判断、骑手圈选粗筛,误差在城市里较大不做计价依据 - 骑行路线距离:高德/腾讯骑行路线规划接口返回的 distance 字段,比较接近实际道路距离,适合作为配送费定价依据
- 实际轨迹距离:根据骑手上报的轨迹点累计计算,用于异常审计与成本核算,不适合用户下单时预估
我的计价规则里有一个动态参数:起步价覆盖3公里,超出部分每公里加价。从用户下单页到后端结算都调用同一个“计价策略”模块,前端需要展示预估费用时就请求后端 calculatePrice 接口,保证同一条规则。
3.4 路线展示与骑手轨迹上报,别在小程序里搞方向盘的伪实时
骑手从取件点出发到送件点的过程,我最初设计是参考打车软件,把轨迹做实时的。实际开发下来发现跑腿场景和打车场景完全不同——打车是一段连续行程,跑腿骑手在同一个商圈可能连续取送多个订单,GPS 信号在楼宇间、电梯内频繁丢失,如果用高韧性的实时轨迹方案,电量和流量消耗立刻会让骑手端小程序变成电老虎。
最终实现采用混合方案:
- 骑行导航过程中高频率上报定位(每5秒一次)
- 静止或停留时暂停上报,检测到位移大于一定阈值再重新上传
- 配送中的轨迹不上墙给用户看,用户端只显示骑手当前实时位置,不显示完整路径
- 骑手端导航路线走地图 SDK 自身能力,不用后端转发路径点
骑手位置推送小程序端则用 WebSocket 长连接,每次位置变化携带 orderNo 坐标和状态位移,前端节流渲染。推送量我用 Redis 做了频道订阅,不同订单订阅不同频道,避免所有骑手位置广播给所有在线用户。
4. 支付与核销:从付款到完单的钱账一致设计
4.1 三种支付时机,你选哪一种
跑腿小程序常见的支付时机有三种:下单后立即支付、跑腿完成后支付、预授权模式。从平台资金安全角度讲,我强烈建议支持“下单即支付”或“余额冻结”,不建议做成纯后付款。否则一个跑腿订单金额几百上千,用户收到货不付钱或者发起争议,平台追款成本极高。
用户在小程序端支付走微信支付 JSAPI,需要一个关键的设计:下单接口与支付统一下单接口分离。
后端在用户点击“立即支付”时用订单号 + 金额发起微信支付下单,拿到 prepay_id,再签名返回给小程序端。小程序端收到参数后调用 wx.requestPayment 拉起收银台。不要在下单业务接口里直接发起支付下单,因为存在重新支付、订单金额被修改等场景,把“创建支付单”作为独立操作会更灵活。
4.2 支付回调处理,讲究的是幂等和状态核对
支付回调是资金链路的核心。微信支付成功后会异步通知你的回调接口,存在重复通知的可能,接口必须幂等。我最开始写回调逻辑时犯过一个错:回调里更新了订单状态并且返回了 success,结果微信重试导致重复入账。后来改成如下结构:
java复制@PostMapping("/pay/notify")
public String payNotify(@RequestBody String xmlData) {
// 1. 验签
Map<String, String> params = WxPayUtil.xmlToMap(xmlData);
boolean signOk = WxPayUtil.verifySign(params, wxPayConfig.getApiV3Key());
if (!signOk) return "签名失败";
// 2. 根据 out_trade_no 查本地支付单
PayOrder payOrder = payOrderMapper.selectByOutTradeNo(params.get("out_trade_no"));
if (payOrder == null) return "支付单不存在";
// 3. 关键:如果已经成功,直接返回成功,避免重复处理
if ("SUCCESS".equals(payOrder.getStatus())) return "OK";
// 4. 比对金额,防止回调金额被篡改
String totalFee = params.get("total_fee");
if (!payOrder.getTotalFee().equals(Integer.valueOf(totalFee))) {
log.error("支付金额不一致,payOrderNo={}", payOrder.getPayOrderNo());
return "金额不一致";
}
// 5. 更新支付单状态
payOrder.setStatus("SUCCESS");
payOrderMapper.updateById(payOrder);
// 6. 发送消息,订单服务异步感知支付成功
rabbitTemplate.convertAndSend("order.exchange", "order.pay.success", payOrder.getOrderNo());
return "OK";
}
这段代码虽然简单,每一步都是资损防线。金额不一致必须告警,而支付单状态的更新和订单状态更新通过消息队列解耦,能避免回调超时导致的连锁故障。
4.3 核销码:安全上要当作凭证而不是装饰
跑腿订单最关键的一步是“核销”。我见过不少团队把核销做成用户在订单详情页点一个按钮“确认收货”就算完。这种方式漏洞很大——如果订单金额是送达后支付,恶意用户完全可以绕过付款直接确认完成。
我的设计是金额高或采用送达后支付的订单,系统生成一个 6 位数字核销码,并同步给用户下单手机号。骑手送达后,需要用户把核销码报给骑手,由骑手在骑手端输入验证,后端校验通过后订单才能流转到“已完成待支付”状态。整个链路是“支付+核销”双凭证缺一不可。
核销码本身不允许明文存储或明文随订单推送给骑手——后端只存储核销码的加盐散列值,校验时再 hash 比对。
java复制// 生成核销码示例
String code = String.valueOf(ThreadLocalRandom.current().nextInt(100000, 999999));
// 存储时绝不存原文
String hash = DigestUtils.md5DigestAsHex((code + salt).getBytes(StandardCharsets.UTF_8));
骑手输入后校验时同样做 hash 再做比较。这样即便数据库泄露,核销码也无法被逆向出来伪造完成状态。
4.4 退款流程与异常订单处理
取消订单和退款几乎是跑腿平台投诉率最高的环节。取消费规则需要提前设计。用户下单且支付后,如果骑手还没有接单,用户可无责取消,全额退款;如果骑手已接单,用户取消需要经过骑手确认或缴纳一定比例取消费,赔付给骑手空跑成本。
退款我尽量走原路退回,核心代码直接调用微信支付的退款接口,同时提前设置好退款回调地址,回调里同步退款状态。有一个地方需要重点提醒:退款必须是一条完整的单独记录,退款金额不能直接改订单总金额。后续财务对账时,通过订单号关联支付单和退款单形成完整链路,会少非常多麻烦。
5. 配送费计价和骑手结算的细节处理
5.1 计价模块如何做到可配置
同城配送计费规则每个城市组合完全不一样,今天起步价调两毛、明天超重规则改一档,如果硬编码在 Java 代码里,每次变更都要发版,运营和开发都会很痛苦。
我这边抽象了一个简易但有效的规则引擎,用 Groovy 脚本做动态编排,运营配置好脚本后通过后台热加载。定价维度包括:距离(按骑行导航距离)、重量区间、时段系数(夜间、雨雪天气)、楼层/电梯费用、小费。
5.2 骑手钱包结算要考虑的坑
骑手端需要展示“今日收入”“待结算金额”“余额提现”。核心问题是钱的归属状态:订单完成后金额进入“可结算余额”还是 T+1 才可提现?为了防止骑手接单后刷单跑路或者服务质量纠纷,我设计的是:
- 订单收入在完成后立刻展示,但是标为“待结算”
- 确认无投诉后 24 小时转入可提现余额
- 提现走微信商家转账到零钱,单笔必须有幂等键
如果你第一版想简化,不搞钱包也可以,直接通过服务商代付接口按日结给骑手打款。但状态意识必须提前设计,要不然后面来补,数据调整的坑会非常痛苦。
6. 高频 Bug 复盘:异常定位、重复支付与消息风暴
6.1 骑手端“位置没有更新”八成是小程序后台保护机制
开发中遇到一个很典型的问题:骑手把小程序切到后台,过段时间再切回来,地图上的位置还是切后台之前那个点。一开始我怀疑是 WebSocket 断了,排查半天发现不是,真正原因在于小程序在后台运行时被系统挂起,定位定时器不再执行。
解决方案分两步:前台路径规划鼓励骑手用地图 App 或 App 内导航,小程序端保持前台运行;同时在小程序切回前台时立即上报一次当前位置并刷新订单状态。后端对骑手位置的过期判断要有策略——连续超过 2 分钟没收到心跳,把骑手状态标记为离线,不再推送新订单给该骑手。
6.2 支付回调风暴导致订单重复发消息
支付成功回调本身并不可怕,可怕的是用了 MQ 异步处理后没有做消费幂等。假设支付服务更新完支付单后发了一条支付成功消息,这条消息因为网络原因在 MQ 里重投了几次,如果消费者不做幂等,就会触发多次“通知用户”的动作,用户能收到几条订单已支付成功的模板消息,体验极其差。
解决起来也不复杂,消费者端要主动做幂等记录:
java复制@RabbitListener(queues = "order.pay.success.queue")
public void onPaySuccess(String orderNo) {
// 幂等控制:同一个订单只处理一次
Boolean processed = stringRedisTemplate.opsForValue()
.setIfAbsent("idempotent:pay:success:" + orderNo, "1", 1, TimeUnit.DAYS);
if (!Boolean.TRUE.equals(processed)) return;
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) return;
// 支付成功后的业务处理:推送骑手、推送给用户
}
并发场景下 Redis 的 setIfAbsent 保证只有一个请求能拿到锁,其余请求直接退出,这是消息消费幂等最省事也最稳的写法之一。
6.3 敏感日志脱敏:别让手机号顺手就打成明文了
跑腿业务里手机号是最高频的数据。接单后骑手需要联系用户,用户需要联系骑手。开发时调试日志里到处是手机号、地址、用户真实姓名,一旦日志被窃取或者排错时截图外发,就是数据泄露事故。
这个属于开发习惯问题,但从工程上可以做绑定约束:全局日志切面把 Mobile 字段统一脱敏,只保留前三位后四位;对象序列化时对标注了 @SensitiveField 的字段脱敏输出;生产日志禁止输出完整取送件地址,只输出经纬度和脱敏后的短地址描述。
7. 第一版上线前,这些测试和缓冲机制能救你一命
整个系统开发到上线时间线上,真正大量踩坑是在压测和灰度阶段。几个容易被忽视但在生产环境炸裂的点:
7.1 模拟并发压出抢单资源竞争
用 JMeter 或 wrk 对“抢单接口”直接模拟 200 个线程同时抢 1 个订单,验证 Redis 预占和数据库乐观锁的表现。如果没有这个测试,直接上线后果可能就是一单多骑手的重大线上事故。我测试时发现 Redis 锁的过期时间设置过短会导致订单明明还在处理,锁却已经释放,第二个骑手进来了。解决办法是锁的自动续期机制——建议锁过期时间设置为 10 秒,而业务处理一般不会超过这个时间,如果业务处理中有远程调用耗时,最好还是引入 Redisson 看门狗机制。
7.2 回调地址必须支持多环境隔离
测试环境的微信支付回调地址指向内网会被微信服务器无法访问,所以小程序端支付调试时一直卡在“支付成功但回调没收到”。正确做法是内网穿透工具把本机服务暴露到公网,同时准备两套微信支付商户号或者至少两个回调路径区分。你甚至可以给回调接口加一个只有微信服务器才知道的密钥头校验,防止有人伪造回调。
7.3 骑手端接单推送别全链路依赖 WebSocket
WebSocket 虽说体验实时,但骑手手机网络切换、小程序被微信杀后台都会导致连接断开。订单推送如果完全依赖 WebSocket 会漏单。我最终实现是双通道策略:
- WebSocket 在线时实时推送并弹窗提醒
- 重要状态变化通过微信订阅消息做兜底(骑手进入小程序时会请求订阅消息授权)
- 抢单大厅数据每次进入页面时主动拉取一次
这套双通道方案虽然无法完全替代原生 App 的推送体验,但在小程序场景下已经把漏单率降到很低,是成本和体验平衡后的可行解法。
8. 这套源码结构里,我最想让你先看哪几个类
抛开那些无用的壳代码,这套 Java 同城跑腿小程序真正最能学到东西的几个模块,我建议按下面的顺序去阅读:
首先看 order/service/OrderStateMachine.java,这是订单状态管理和流转的核心,能理解状态机驱动业务所有动作的思想。其次是 dispatch/service/GrabOrderService.java,重点看 Redis 预占与数据库乐观锁怎么配合,怎么写才能在高并发下不错不乱。然后是 pay/service/WxPayCallbackService.java,搞懂回调幂等和金额校验细节,这个类是资金安全的核心阵地。最后是 rider/RiderLocationService.java,看如何在上报频率、业务判断和数据库压力之间取得平衡。
跑腿业务看起来是个体力活撮合平台,后端技术深度却一点也不浅。你把订单状态机、并发控制、地理坐标处理、支付回调幂等这几件事想得足够清楚,这套系统才谈得上具备可上线的基本条件。源码拿来直接改改能跑只是第一步,把数据库里的钱和用户的订单搞清楚了,才算是真正吃透了一个同城跑腿小程序的核心价值。
前阵子有个朋友跟我说,他用这套源码跑通了 demo,上线试运营第一天就遇到了骑行距离虚高导致配送费翻倍的投诉——原因就是导航接口返回的 distance 在部分城市有额外绕路算法,而我提供的计价模块里默认取了原值没做阈值限制。这类边界问题你不可能靠读文档全部预见,只能在实际运营里不断调优。你准备好进入这个需要持续打磨的领域了吗?代码只是个起点而已。
