
简介一套基于Java SpringBoot与GraphHopper库构建的多途径点路径规划系统源码面向有一定Java后端基础、需要实现物流配送、多点导航或巡检路线优化等场景的开发者。压缩包共23个文件主要包含4个Java源文件、2个OSM地图数据文件、Maven构建配置pom.xml及mvnw脚本、GraphHopper运行所需的索引与图数据文件如nodes、edges、location_index等以及若干配置说明文档整体约31.72MB。当前已有325人学习下载。这套源码完整展示了SpringBoot集成GraphHopper的关键流程从OSM地图数据加载、路网图构建到多途经点查询接口的封装同时给出了路径计算所需的核心参数配置与图数据持久化方案。开发者可直接参考项目结构理解各文件在路径规划链路中的具体作用并基于现有代码扩展业务逻辑、调整路径算法策略或接入新的地图数据源适合作为二次开发、毕业设计或课程设计的基础工程。1. 多途径点路径规划不是地图 API 的活GraphHopper SpringBoot 能自己干接到一个配送调度需求某快递站每天有三十多个派件点车辆从站点出发送完所有点再回站之前一直用地图厂商的路线 API 挨个算相邻点耗时再人工排顺序排一次要半小时高峰时根本排不过来。我接手后第一反应是找路线规划 SDK对比了一圈发现自托管的 GraphHopper 是投入产出比最高的方案——它是纯 Java 写的开源路由引擎能直接嵌进 SpringBoot 工程吃 OpenStreetMap 格式的道路数据本地就能批量算两点间耗时多点顺序规划则是在它提供的耗时矩阵接口之上再套一层优化算法把“先算距离再排顺序”两步彻底自动化。这套东西打通之后排三十个点的路线从半小时压缩到几秒不用为每次调度付 API 调用费也没有外网依赖数据全在本地。核心组件只有三个GraphHopper 负责路网图构建和单点路由计算、SpringBoot 负责把引擎包成 HTTP 服务、TSP 顺序优化算法负责把多途径点排序。适用对象很明确自建配送调度、外勤巡检、农机田间路径规划这类需要批量、反复计算多点路线的系统。看完这篇你能把最小可跑的 GraphHopper 服务嵌进自己的 SpringBoot 工程并亲手实现一个多途径点顺序规划接口。2. GraphHopper 选型与数据准备为什么是它而不是别家路由引擎2.1 自托管路由引擎的定位先搞清楚 GraphHopper 能干什么GraphHopper 是一个 Java 实现的开源路由引擎核心能力有三块解析道路数据、构建路网图、按不同交通方式计算两点或多点之间的路线与耗时。它不是在线服务而是一个嵌入式的 Jar 包你的 SpringBoot 进程启动时调用它的 API 就能完成路网加载之后每次路由请求都在本地内存里计算不走任何外呼。选它之前我对比过几种方案。纯调用在线地图厂商的路线 API 当然最省事但多途径点场景下要反复调用批量接口费用和配额都是问题而且有些业务数据不能出内网。另一类是自建 OSRM 服务它性能很强但有独立的 C 进程和 HTTP 服务和 Java 工程是两套体系部署运维要多管一个组件。GraphHopper 和 SpringBoot 是同语言、同进程路网和路由逻辑全在一个 JVM 里对单机几千点规模以内的业务足够用部署也最简单。架构上它把“路网数据文件”和“内存中的图对象”分开处理启动时一次性读取.osm.pbf格式的道路数据解析后构建十字路口节点和路段边组成的图路由请求时在这个图上做双向 Dijkstra 或 A* 搜索。这里说的图是图论里的图不是图片存储结构和检索效率直接决定路由耗时。你的 SpringBoot 服务里只需持有 GraphHopper 单例所有请求线程共享同一个图对象这也是它嵌入式用法高效的原因。2.2 道路数据怎么落到本地从 .osm.pbf 到 GraphHopper 图GraphHopper 要跑起来第一步是准备道路数据。最常见的数据格式是.osm.pbf它是 OpenStreetMap 数据的二进制压缩格式体积比同内容的 XML 小好几倍解析也更快。国内大区级路网数据通常几百 MB全国级需要几个 GB不要一上来就想全量加载按业务覆盖范围裁剪数据更务实。数据来源渠道有几种但不要下载整个国家的文件塞进内存启动时构建图会非常慢且吃内存。我一般先按城市或省份边界裁剪用 osmium 这类命令行工具可以把大区域切成小区域只保留业务涉及的范围。GraphHopper 虽然自带数据处理工具但裁剪在数据准备阶段做更灵活后面改业务边界也不用重新构建整个图。拿到.osm.pbf后GraphHopper 需要把它“导入”并构建成自己的图存储格式。运行时配置解析器会读取道路标签等级、限速、单行道等然后做节点合并和边缘计算。这个构建过程耗时取决于数据量一个城市级文件通常在几十秒到几分钟区域级可能要几个小时。构建好的图会写到本地目录下次启动如果目录存在且数据没变就直接加载序列化后的图启动时间从几分钟降到几秒。2.3 图构建的三个核心参数图存储目录、交通方式、内存映射模式GraphHopper 的初始化参数直接决定内存占用和路由准确性。第一个是图存储目录指定后引擎会把构建好的图落盘重启不用重新解析原始道路数据。第二个是交通方式GraphHopper 的每种交通方式对应一套道路可走性与速度配置汽车模式跳过步行道、走机动车道步行模式则是另一套规则选错模式会导致路线完全不同。内存映射有两种模式RAM_STORE把整个图载入堆外内存路由最快但数据量大时占用偏高MMAP是内存映射文件按需加载适合图较大而内存有限的环境。单机并发不高、图在 500 MB 以内用 RAM_STORE图超过 1 GBMMAP 更稳妥能省下大量内存。我通常会关闭CHContraction Hierarchies预处理——预计算会让单点路由快一个数量级但对多途径点的矩阵批量计算场景CH 加速效果并不明显反而增加构建时间和内存。多途径点规划的核心是频繁调用路径替代算法算矩阵用不带 CH 的普通图更直接。看一下典型的初始化配置代码在用 SpringBoot 之前先在独立方法里验证参数是否合理// 初始化 GraphHopper 引擎核心配置项 public GraphHopper initGraphHopper() { GraphHopper hopper new GraphHopper(); // 数据目录存放 .osm.pbf 原始数据 hopper.setOSMFile(/data/osm/beijing-latest.osm.pbf); // 图存储目录构建后的图落盘位置 hopper.setGraphHopperLocation(/data/graph-cache/beijing); // 编码方式支持汽车、步行等多种方式逗号分隔 hopper.setEncodedValuesString(car,foot); // 内存映射模式有 RAM_STORE 和 MMAP 两种可选 hopper.setMemoryMapped(false); // 不启用 CH 预处理降低构建开销 hopper.setCHEnabled(false); hopper.importOrLoad(); return hopper; }代码里setEncodedValuesString定义图里预计算哪些编码属性car 和 foot 是两种最常见的编码方式对应不同道路通行能力。importOrLoad()是关键方法数据没导入过就执行导入并保存导入过就直接加载。这个过程会打印详细的日志第一次运行观察日志里的导入进度和耗时能帮你判断数据量级是否匹配当前机器配置。3. SpringBoot 里把 GraphHopper 跑起来从配置类到可调用的路由接口3.1 依赖与 Bean 初始化让引擎随应用生命周期启动SpringBoot 工程引入 GraphHopper 很简单核心就一个依赖com.graphhopper:graphhopper-core。版本要和你下载的源码对应这一点容易翻车GraphHopper 的 Java 版本要求偏高JDK 8 在较新版本上会直接运行失败建议 JDK 11 或 17。另一个隐蔽问题GraphHopper 依赖了若干第三方库如果你的工程里也用了低版本冲突库会出现NoSuchMethodError启动时注意看异常栈优先调整 GraphHopper 传递依赖的版本即可。Bean 初始化上用Configuration定义一个GraphHopper单例Bean(initMethod importOrLoad)让它随 Spring 容器启动自动完成图加载。不要每次请求都 new 一个 GraphHopper那等于每次重新加载路网内存会爆。下面是标准的初始化配置类Configuration public class GraphHopperConfig { Value(${graphhopper.osm-file}) private String osmFile; Value(${graphhopper.graph-location}) private String graphLocation; Bean(initMethod importOrLoad, destroyMethod close) public GraphHopper graphHopper() { GraphHopper hopper new GraphHopper(); hopper.setOSMFile(osmFile); hopper.setGraphHopperLocation(graphLocation); hopper.setEncodedValuesString(car); hopper.setMemoryMapped(true); hopper.setCHEnabled(false); return hopper; } }配置项被拆分到了application.yml便于不同环境切换数据目录。注意destroyMethod close应用停机时释放底层文件句柄和内存映射资源反复重启部署时不会出现文件锁问题。setMemoryMapped(true)对大图更友好但首次路由请求会偏慢因为页面数据是懒加载的预热一次就好。启动后日志里如果出现Using memory mapped files和Finished loading graph说明图加载成功。之后所有线程共享该单例路由计算是并发的但引擎内部对图读取做了线程安全处理不需要额外加锁。3.2 封装单点路由计算从经纬度到路线详情最基础的能力是两个经纬度之间的路线。调用GraphHopper是GHRequest/GHResponse模式创建请求、设置起终点经纬度、指定编码方式、发送到引擎然后检查响应是否有异常。这里有一个最容易忽视的坐标顺序问题GraphHopper 的 API 统一是纬度在前、经度在后也就是(lat, lon)和国内很多系统习惯的(lng, lat)正好相反。我封装的服务类接收业务侧的(lng, lat)在进入 GraphHopper 之前转成(lat, lng)避免调用方因为坐标顺序搞混而返回完全离谱的路线。完整代码Service public class RouteService { private final GraphHopper graphHopper; public RouteService(GraphHopper graphHopper) { this.graphHopper graphHopper; } public RouteResult route(double fromLng, double fromLat, double toLng, double toLat) { // GraphHopper 使用纬度在前经度在后的顺序 GHRequest request new GHRequest(fromLat, fromLng, toLat, toLng); request.setProfile(car); // 限制搜索范围避免歧义点导致路由绕远 request.setHint(ch.disable, true); GHResponse response graphHopper.route(request); if (response.hasErrors()) { String errorMsg response.getErrors().toString(); throw new IllegalStateException(路由计算失败: errorMsg); } ResponsePath path response.getBest(); return new RouteResult(path.getDistance(), path.getTime(), path.getPoints().toLineString(false).toString()); } }setProfile(car)指定用哪种编码方式必须是在初始化时setEncodedValuesString里声明过的否则抛异常。path.getPoints().toLineString(false)把轨迹点序列转成 WKT 格式的 LineString 字符串前端地图直接按这个串绘制路径省去自行序列化的麻烦。getDistance()返回的是米getTime()返回的是毫秒两个值后续计算矩阵时都要用到。这个接口本身不复杂但它验证了整个 SpringBoot 集成链路是否通——配置类有没有加载、坐标转换有没有问题、GraphHopper 能不能算出路线。集成新引擎时务必要先把单点路由跑通再谈多途径点。3.3 距离矩阵计算多途径点规划的数据底座单点路由只能解决 A 到 B多途径点规划需要知道任意两点之间的耗时于是 GraphHopper 的矩阵计算接口就该登场了。它避免你写双层循环逐个调用单点路由——一次请求算出所有点对之间的耗时和距离内部会做批量优化耗时只有逐个调用的几分之一。GraphHopper 矩阵 API 要构建一个MatrixRequest设置所有点的经纬度、编码方式然后执行。返回的矩阵是二维数组下标与原数组一一对应。这里要注意MatrixRequest不是GHRequest它在底层会复用团簇算法对区域内的点做批量计算所以如果业务上确实只需要某几个点之间的两两耗时用矩阵接口比循环单点路由合适得多。public long[][] getTimeMatrix(ListPoint points) { ListGHPoint ghPoints points.stream() .map(p - new GHPoint(p.getLat(), p.getLng())) .collect(Collectors.toList()); MatrixRequest matrixRequest new MatrixRequest(); matrixRequest.setPoints(ghPoints); matrixRequest.setProfile(car); // 只需要耗时数据不返回路径几何形状降低传输和计算开销 matrixRequest.setOutArrays(Arrays.asList(time, distance)); matrixRequest.setFailFast(false); MatrixResponse matrixResponse graphHopper.matrix(matrixRequest); long[][] result new long[ghPoints.size()][ghPoints.size()]; for (int row 0; row matrixResponse.getTimes().length; row) { // 逐行拷贝耗时矩阵行下标是起点列下标是终点 result[row] matrixResponse.getTimes()[row]; } return result; }setOutArrays只声明time和distance别再加weight这类内部字段多了白白增加计算量。setFailFast(false)很关键矩阵中只要有一个点无法到达整个请求会直接失败把它关掉后能拿到部分结果哪些点不可达在响应里标记出来方便业务层单独处理异常点。矩阵结果保存在内存中几十个点时就是一个N x N的二维数组后续顺序优化算法直接基于这个矩阵运算不再碰 GraphHopper。4. 多途径点顺序规划耗时矩阵 最近邻 2-opt 是性价比最高的组合4.1 把多点问题拆成 TSP为什么要先算矩阵再排顺序、不能直接在图上搜索明确了矩阵接口之后核心问题浮现给三十个派件点和一个起点怎么决定先送哪个、后送哪个路线规划问题的本质是旅行商问题TSP——找一个经过所有点且总耗时最短的顺序。注意这不是在地图图上做一次简单搜索能解决的因为需要经过的点有多个而且顺序未知搜索空间是排列组合级别的。图搜索如 A*只能解决“从 A 到 B 的最短路径”解决不了“经过 A、B、C、D 并以 E 结束的最短路径顺序”。TSP 的解法分精确算法和启发式算法。精确算法如动态规划 Held-Karp在 20 个点左右就开始吃力几十上百点的场景完全不可用。实际工程里贪心最近邻给出一个不错的基础解再用 2-opt 局部搜索优化这种组合在几十点规模下能跑到接近最优耗时也短是性价比最高的方案。更复杂的模拟退火和遗传算法在这个规模下收益有限反而让代码复杂度和调参成本大幅上升。业务上还要考虑起点和终点的约束。最常见场景是“从站点出发送完所有点再回站点”这是一个闭合 TSP起点和终点都是站点本身另一种是“不回站点送完就地收工”这就是路径 TSP。约束不同算法里的矩阵取值方式就不同——闭合 TSP 在计算总耗时时要加上“最后一个点回到起点”的耗时。4.2 先用最近邻快速生成初始顺序几分钟的高质量可行解最近邻的思路很朴素从起点出发每次选择离当前点耗时最短的下一个未访问点走完所有点后得到一条路径。它本质是贪心策略单次运行结果是确定的执行时间只有几毫秒到几十毫秒取决于点数解的质量通常不超过最优解的 20% 偏差作为 2-opt 的初始解完全够用。关键点在于矩阵的构建方式——我们需要拿到任意两点之间通过路网的真实驱动耗时而不是直线距离。这就是第 3 章里矩阵接口的价值把三十个点的经纬度一次性传入GraphHopper 返回两两之间的驾驶耗时这个耗时已经考虑了路网、红绿灯和道路等级比欧氏距离靠谱得多。最近邻算法实现如下public ListInteger nearestNeighbor(int startIndex, long[][] timeMatrix) { int n timeMatrix.length; boolean[] visited new boolean[n]; ListInteger route new ArrayList(); int current startIndex; route.add(current); visited[current] true; while (route.size() n) { int next -1; long minTime Long.MAX_VALUE; for (int i 0; i n; i) { if (!visited[i] timeMatrix[current][i] minTime) { minTime timeMatrix[current][i]; next i; } } if (next -1) { // 剩余的节点不可达直接退出并保留部分路径 break; } route.add(next); visited[next] true; current next; } return route; }timeMatrix[current][i]的语义是“从当前节点到节点 i 的耗时”所以贪心选择时只需要扫描当前行。startIndex是起点在矩阵中的下标需要在矩阵接口调用前维护好“原始点列表”和“下标”的对应关系规划完成后按这个关系把下标翻译成真实的经纬度点即可。不可达点时直接 break 是因为failFast(false)之后矩阵里会有Long.MAX_VALUE继续循环只会卡死不如提前结束并把这个情况上报给业务层。4.3 2-opt 局部搜索用两两交换把路径磨到接近最优最近邻得到一个初始解但问题很明显贪心的短视行为经常导致某条路走了回头路或者绕了一个大圈。2-opt 的思想是取当前路径中的两条边断开后反向连接重排如果新路径的总耗时更小就采纳。反复执行直到没有可以改进的交换为止收敛之后路径基本定型。2-opt 最核心的改进点是“倒序”操作。看当前路径序列如果交换位置i和j之间的路径段需要把这段倒过来而不是简单换两个节点——因为路径中某段的方向决定了车辆实际走的道路方向。用路网耗时矩阵检查倒序前后的总耗时保留更优的那个。2-opt 嵌入实现public ListInteger twoOpt(ListInteger route, long[][] timeMatrix) { ListInteger best new ArrayList(route); long bestTime calculateTotalTime(best, timeMatrix); boolean improved true; while (improved) { improved false; for (int i 1; i best.size() - 1; i) { for (int j i 1; j best.size(); j) { ListInteger candidate twoOptSwap(best, i, j); long candidateTime calculateTotalTime(candidate, timeMatrix); if (candidateTime bestTime) { best candidate; bestTime candidateTime; improved true; } } } } return best; }twoOptSwap把[i, j]这段反转后重新拼接实现方式很常规前半段保留中间段倒序后半段保留。calculateTotalTime按顺序遍历路径累加相邻节点耗时如果路径是闭合 TSP还要把最后一个点和第一个点的耗时也加进去——这是调参时最容易漏掉的一块。2-opt 的单轮时间复杂度是 O(n²)加上外层循环在 30 个点的规模下通常是几十毫秒级的完全够用。它做不了全局最优但能保证解质量在 5% 偏差以内对绝大多数配送调度场景绰绰有余。4.4 多途径点接口落地把矩阵和优化算法串成一个 HTTP 接口整个流程串到 SpringBoot 里对外暴露一个 HTTP 接口接收一组经纬度点返回排序后的路线。这里我增加了一个“强制回起点”开关因为很多业务场景是车辆出去必须回站点也有场景是送完就地下班两种需求用一个开关控制算法实现上只在总耗时计算时多加/少加一条回到起点的边。接口设计考虑了批量规划场景的调用方式一次传入所有点服务端同步计算返回带有顺序标记的路线列表。由于矩阵计算和 2-opt 都在内存里跑几十点级别的接口响应时间通常在数百毫秒足够交互式使用。RestController RequestMapping(/api/route) public class MultiPointRouteController { private final RoutePlanningService planningService; public MultiPointRouteController(RoutePlanningService planningService) { this.planningService planningService; } /** * 多途径点路径规划接口 * param points 起点 途径点列表 * param returnToStart 是否返回起点 */ PostMapping(/plan) public PlanResult plan(RequestBody PlanRequest request) { ListPoint points request.getPoints(); boolean returnToStart request.isReturnToStart(); return planningService.planOrder(points, returnToStart); } }RoutePlanningService.planOrder内部依次执行转换 GHPoint 列表、调用矩阵接口、最近邻生成初始顺序、2-opt 优化、把下标翻译成经纬度、计算总耗时。整个流程把 GraphHopper 引擎、矩阵接口和 TSP 算法串成一个业务闭环。服务层接口返回值里包含排序后的点列表、每段路线耗时和总耗时业务方可以直接按这个结果生成调度单或导航路线。planOrder 核心流程拆解步骤操作数据来源1转换坐标点lng/lat 转 lat/lng请求参数2构建耗时矩阵GraphHopper Matrix API3最近邻生成初始顺序耗时矩阵42-opt 优化顺序初始顺序 耗时矩阵5翻译下标 → 经纬度原始点列表6计算总耗时、返回结果优化后顺序你会发现真正的规划逻辑并不在 GraphHopper 里而是发生在这个服务类中。GraphHopper 只负责回答“这两点之间开车多久”顺序怎么排是你自己的 TSP 算法说了算。5. 避坑GraphHopper 集成里最常见的 5 个翻车点5.1 坐标顺序搞反路线全部横穿地图现象从北京到天津的路线算出来横跨了中国版图或者路线自动绕了上千公里。原因GraphHopper 的 GHPoint 构造器明确是(lat, lon)很多业务系统主键存的是(lng, lat)直接反转传入后本应向北的路变成了往东图搜索完全错乱。解决在进入引擎之前统一做一次坐标转换并且写单元测试锁定给定已知经纬度的 A/B 两点验证计算出的距离在合理范围内。我吃过一次亏之后在这个转换方法里加了强校验——如果纬度超过 90 或者经度超过 180直接抛异常宁可开发期炸掉也不要线上错。5.2 道路数据精度不够压根搜不到点现象某几个点报Could not find point但经纬度看着明明在市区。原因.osm.pbf 数据的精度和详细程度取决于数据源本身。有些区域道路缺失或断头路太多GraphHopper 在图上找最近的可行走边失败就会报点不可达。这种情况在郊区、新建开发区特别常见。解决先把点投影到图上查看GraphHopper返回的最近边和投影点确认是否有道路数据如果有路但断头则手动补录几条道路或者调整点坐标到实际道路位置。工程上给业务方提供一个“地图选址”功能让他们在地图上选点而不是填坐标能从源头规避这个坑。5.3 强制回起点时总耗时少算一段现象接口返回的总耗时明显偏短对比实际跑下来的时间差了将近半小时。原因闭合 TSP 要求“最后一个点回起点”这一段路的耗时也要计入总和但实现里只算了路径内部相邻点的耗时少了最后一段返程。这个在代码 review 阶段特别容易漏因为看起来路径已经“结束”了。解决在calculateTotalTime方法里增加一个布尔参数returnToStart闭合时把timeMatrix[last][first]加上。同时接口返回的明细里单独列出一个“返程耗时”字段让业务方能看出这半小时是怎么来的。5.4 CH 预计算打开导致启动极慢、内存吃紧现象启动时构建图明显变慢日志显示 CH preprocessing 卡了很久内存占用翻倍。原因和第 2 章说的一样CH 预计算会为每个节点算跳表构建时间和内存开销都很高。多途径点场景下主要调矩阵接口单次路由用的不多CH 收益有限但它造成的构建负担是实打实的。解决初始化配置关掉 CHhopper.setCHEnabled(false)。如果确实有个别高并发单点路由需求可以单独为那个接口准备一个带 CH 的引擎实例两个引擎共享同一份图数据目录互不干扰。5.5 内存映射模式下首次请求特别慢现象服务启动后第一个路由请求要等好几秒甚至几十秒后面的请求就正常了。原因MMAP 模式下图数据是懒加载到内存的哪条路被搜索到了才把那部分数据页加载进来。首次请求覆盖的路段多相当于把所有道路数据都触达了一遍自然慢。解决应用启动后做一次“预热”调用随便算一条短线路让底层把常用图页加载完。或者直接用RAM_STORE模式启动时全部载入第一次请求和后面一样快。内存够的话优先 RAM_STORE省心。6. 进阶加一个“一键优化”按钮前先搞懂这些技巧多途径点规划从能跑到好用中间还有几个提升体验的小改动。第一个是把 2-opt 的迭代上限露出来。当途径点数超过五十个时完全收敛可能要几十轮而实际业务里 2-opt 前几轮效果最明显后面几轮收益很小。我习惯设置一个最大迭代次数比如 5 轮配合improved标志位耗时可控解质量也只有百分之几的差距。第二个技巧是复用矩阵计算结果。同一个小区的多个配送点耗时矩阵可能长时间不变而 GraphHopper 的矩阵计算是有成本的尤其点多的情况下。如果你的输入点集相对固定把矩阵结果缓存起来点集变化时才重新计算。可以用简单的ConcurrentHashMap加一个版本号来实现点在缓存里就直接跳到 TSP 优化阶段。第三个技巧是异步化长任务。五十个点以上的规划矩阵计算加 2-opt 可能要几秒同步阻塞 HTTP 请求会让前端一直转圈。一个老板键做法接口先返回任务 ID后端用线程池执行规划完成后把结果写到内存缓存前端轮询拿结果。这和直接用同步接口相比用户体验差别明显。车辆容量约束是另一个常见扩展方向。目前算法只优化耗时不关心车能不能装下所有货。如果你的业务里每辆车有载重上限那这就从 TSP 变成了 CVRP带容量约束的车辆路径问题需要在排顺序时同时检查已分配点的累计重量。最简单的做法是分桶——按顺序贪心塞进多辆车每辆车满负荷后开下一辆再用 2-opt 分别优化每辆车的顺序。我做这套系统的过程中最耗时的不是算法本身而是把坐标顺序、数据精度、返程耗时这些细节打磨对。这些坑不填掉再快的最优算法都给不了业务可用的结果。规划接口上线之后配送员的实际行驶距离和我算出来的路线差距稳定在 10% 以内——说明路网速度假设和实际匹配良好。后续如果你遇到“某些点搜不到”“总耗时不对”“启动慢得离谱”这类问题按第 5 章的排查顺序过一遍大多数能直接解决。如果你要更大规模的点集上千个点那种建议改用专门的 VROOM 和 jsprit 这类开源求解器配合 GraphHopper 做路网前处理依然能把整套链路留在自己的工程里。希望帮到你。本文还有配套的精品资源点击获取