导航功能开发博客 3:实时状态、偏航判断与兜底机制
篇解决的就是导航系统最核心的工程问题怎么让导航随着用户移动而持续变化并且在某个通道失效的时候功能不至于整体崩掉。做这步之前我写过的东西基本都算 demo——请求发出去、结果返回来。但导航不一样它必须持续在线、持续判断、持续纠错。一、导航为什么必须是实时的导航和普通查询接口最大的区别在于它不是返回一次就结束的。用户一边走系统一边要回答这些问题——当前位置变了吗还在路线上吗离终点还有多远现在该提示什么到了吗该停了吗所以我在后端把导航定义成一个可持续更新的NavigationState它不是一次性返回的结果对象而是一个随位置变化持续演进的状态对象。这在navigation_service.py里体现得很直接start_navigation()创建会话和初始状态update_location()接收位置变化并重新计算整个状态get_status()供轮询时读取快照stop_navigation()结束会话并广播停止事件。一开始我把update_location()写得很简单——只更新坐标不重新算偏航和步骤索引。结果导航开始之后页面上的提示文字就再也不变了即使人已经走出两条街。这让我意识到位置上报和状态计算是两条必须并行的线前端负责采集位置后端负责判断位置的意义缺一不可。二、位置上报前端采集后端决策导航开始后前端会启动一个定时器每隔一段时间读取当前定位并上报后端startLocationLoop() { this.stopLocationTimer(); const interval this.data.navSettings.navUpdateIntervalSec || 2; this.locationTimer setInterval(() { if (!this.data.sessionId || !this.data.isNavigating) return; wx.getLocation({ type: gcj02, success: (res) { this.updateLocation(res.latitude, res.longitude); }, fail: (err) { navLog(定位失败, err); } }); }, interval * 1000); },这段代码看着平淡但有个细节值得说上报间隔navUpdateIntervalSec是从设置里读的不是写死的。最初我硬编码了 2 秒后来在室内测试时发现定位精度差、频繁上报反而导致导航提示来回跳——明明站在原地系统一会儿说偏航一会儿说已回到路线。把间隔做成可配置之后我可以在室内测试时把间隔调长到 5 秒减少抖动室外精度好的时候再调回来。前端只上报原始坐标不做任何判断。这个分工很重要前端知道我现在在哪后端判断我在路线的什么位置。如果前端也做判断比如自己算偏航那后端的偏航状态和前端的偏航状态很可能不一致到时候两边打架bug 更难查。三、偏航和到达导航系统的核心判断偏航判断和到达判断是写导航服务时最有意思的部分因为这才是真正导航逻辑所在。到达判断比较直接当前点到目的地的 Haversine 距离小于到达阈值默认 12 米就认为到达。偏航判断要复杂一些。我用的方法是计算当前点到路线点集中最近点的距离超过阈值默认 25 米就判定偏航。这个方法不算精确——理想情况下应该计算点到线段的垂直距离而不是点到点的距离——但 V1 阶段够用后面可以迭代。async def update_location(self, session_id, lat, lng, headingNone, speedNone): session navigation_store.get_session(session_id) if not session: raise ValueError(导航会话不存在) session.current_lat lat session.current_lng lng session.updated_at int(time.time()) distance_to_destination haversine_m(lat, lng, session.destination_lat, session.destination_lng) arrive_threshold session.settings.arrive_threshold_m or NAV_ARRIVE_THRESHOLD_M session.arrived distance_to_destination arrive_threshold session.is_off_route False if session.arrived: session.current_instruction 已到达目的地 session.is_navigating False else: session.is_off_route self._detect_offroute(lat, lng, session) if session.is_off_route: session.current_instruction 您已偏离路线请调整方向 else: session.current_step_index self._update_step_index(session, lat, lng) session.current_instruction self._current_instruction(session, session.current_step_index)有个细节偏航检测之后我先is_off_route False再根据条件赋值。这是因为每次位置更新都要重新判断——上次偏航不代表这次还偏航用户可能已经走回路线了。如果不清零偏航状态就会粘住永远变不回来。这段逻辑让我真正意识到导航系统的本质不是画线而是判断和反馈。画一条路线不难难的是持续跟踪用户有没有跟着这条线走。四、双通道WebSocket 轮询兜底我做实时功能一上来只用 WebSocket。一开始也是这么想的。但真机测试的时候遇到了几个问题校园 Wi-Fi 不稳定WebSocket 连着连着就断了微信小程序的 WebSocket 在某些机型上行为不一致后端开发服务器偶尔会重启长连接直接断掉断了之后页面完全失去响应不知道导航还在不在继续如果只有 WebSocket 一种通道断连就是功能停摆。所以我做了双通道优先 WebSocket 推送WebSocket 失败后自动切换到 HTTP 轮询。前端逻辑拆成了几个方法tryConnectWebSocket()尝试建立 WebSocket 连接switchToPolling()在 WebSocket 断连时切换到轮询模式startFallbackPolling()启动周期性状态查询queryNavigationStatus()拉取最新状态并刷新页面tryConnectWebSocket(sessionId) { if (!sessionId) return; this.setData({ connectionMode: ws, connectionHint: 正在尝试 WebSocket 连接..., wsConnected: false }); this.closeWebSocket(); const wsUrl ${WS_BASE}${NAV_WS_PATH}?session_id${sessionId}; try { this.ws wx.connectSocket({ url: wsUrl }); } catch (error) { this.switchToPolling(WebSocket 创建失败已切换轮询); return; } }tryConnectWebSocket里有一个容易忽略的细节先closeWebSocket()再创建新连接。这是因为微信小程序的 WebSocket 有连接数限制如果不关旧的直接开新的几次下来就会报超过最大连接数。我是在连续快速重启导航时踩到的这个坑。这套双通道机制的意义不只是防止报错而是让导航具备了工程韧性。对于一个要面向真实用户的功能来说大部分时候能用和什么时候都能用是两个完全不同的标准。五、后端 WebSocket 推送的工作方式前端只是接收端真正发消息的是后端。我在ws_manager.py里写了一个简单的连接管理器class WebSocketManager: def __init__(self): self._connections: DefaultDict[str, List[WebSocket]] defaultdict(list) async def connect(self, session_id, websocket): await websocket.accept() self._connections[session_id].append(websocket) def disconnect(self, session_id, websocket): if session_id in self._connections and websocket in self._connections[session_id]: self._connections[session_id].remove(websocket) if not self._connections[session_id]: self._connections.pop(session_id, None) async def broadcast(self, session_id, message): connections list(self._connections.get(session_id, [])) for websocket in connections: try: await websocket.send_json(message) except Exception: self.disconnect(session_id, websocket)这个管理器很简陋。它的核心职责就是按 session_id 分组管理连接状态变化时广播给同一个 session 的所有客户端。broadcast里我遍历的是list(self._connections.get(...))而不是直接遍历原列表因为在广播过程中如果有连接断开disconnect会修改_connections直接遍历会抛运行时错误。导航状态变化时服务层会主动广播以下事件navigation_started— 导航开始navigation_update— 位置更新navigation_offroute— 偏航navigation_arrived— 到达navigation_stopped— 手动停止后端不是被动等前端来问而是状态一变就主动推出去。这是实时系统和请求-响应系统最根本的区别。但有时用户位置未变后端状态确不断推送到前端这是需要修复的bug。六、为什么到达判断放在后端这个问题我后来想得很清楚。如果只靠前端判断到达会有几个问题首先前端定位精度在不同设备上差异很大。有些手机在室内误差能有几十米如果前端自己判断到达阈值很难统一——12 米在某些设备上永远触发不了在另一些设备上可能提前触发。其次如果前端自己判断到达然后直接结束导航流程后端的会话状态还是导航中WebSocket 还在推navigation_update事件两边状态就分裂了。再者到达是一个需要全局生效的事件——它意味着会话结束、WebSocket 要关闭、前端要切回初始状态。这个决策权应该在后端前端只负责根据后端返回的状态来更新界面。七、三篇博客回顾三篇博客对应了导航功能的三个阶段骨架页面结构、会话管理、接口定义、状态模型——先让数据能跑通可视化marker、polyline、提示卡片、信息组织——让状态能被看见实时化位置上报、偏航判断、到达判断、双通道通信——让系统能持续在线走到这里导航功能已经不是一个简单的地图页面而是一个有工程结构的子系统了。