ai-coding
2026-06-30 403bea45afb5f0dae1170f3a50bb54a6c0ec87ff
docs: 修复目的地流程梳理

Co-Authored-By: 卢春龙 <lucl@xiaopeng.com>
1个文件已添加
218 ■■■■■ 已修改文件
docs/修改目的地流程梳理.md 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/修改目的地流程梳理.md
New file
@@ -0,0 +1,218 @@
# 行程中修改目的地 — 完整逻辑梳理
---
## 一、整体流程概览
```
用户点击"修改目的地"
  → 写入 session 缓存
  → 跳转选点页
  → 用户选终点
  → 写入 candidate 缓存
  → reLaunch 回行程页
  → 页面恢复 + 预览态恢复
  → 展示预览确认卡片(新路线 + 新费用)
  → 用户确认 / 取消
    → 确认:调后端 orderChange → 更新终点 → 退出预览态
    → 取消:恢复旧终点 → 退出预览态
```
---
## 二、入口条件
| 条件 | 说明 |
|------|------|
| 按钮可见 | `tripStatus` 为接驾中(4)、车辆已到达(5)、行程中(6) |
| 按钮可点击(非置灰) | 以下条件全部不命中:<br>① socket 推送 status=0(临近终点)<br>② 剩余距离 < `modifyDistanceLimitMeters`<br>③ 剩余时长 < `modifyDurationLimitMinutes` |
**代码位置**:`index.tsx` 第 1139-1177 行 `modifyDestinationDisabledToastText`
---
## 三、阶段一:启动流程(handleStartModifyDestination)
**文件**:`hooks/use-modify-destination-flow.ts`
1. 写入 **session 缓存**(storage key: `modifyDestinationSession`):
   ```json
   {
     "active": true,
     "previousEndSite": "<当前终点>",
     "previousFareEstimate": "<当前费用>",
     "orderId": "<当前订单ID>",
     "tripStatus": "<当前行程状态>",
     "updatedAt": 1234567890
   }
   ```
2. 写入 **路线点缓存**(`modifyDestinationRoutePoints`):当前路线点 → 用于取消后回滚路线
3. 清空 **候选终点缓存**(`modifyDestinationCandidate`)
4. 设置流程状态 → `SELECTING`
5. 调用 `toSelectSite('end')` → `Taro.navigateTo` 到站点搜索页
---
## 四、阶段二:选点页选终点
**文件**:`pages/site-search/index.tsx`、`pages/select-map-site/hooks/use-select-map-site.ts`
用户选择新终点后:
1. 判断新终点是否与原终点相同 → 写入 `modifyDestinationPendingSameSiteToast` 标记
2. 将选中站点写入 **候选终点缓存**(`modifyDestinationCandidate`)
3. 写入跳过空调查询标记(`modifyDestinationSkipAcQueryOnReturn`)
4. 通过 **`Taro.reLaunch`** 返回行程页:
   ```
   /robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1
   ```
   > 注意:是 reLaunch(销毁全部页面栈重新创建),不是 navigateBack
---
## 五、阶段三:行程页恢复 + 预览态建立
### 5.1 页面恢复(use-page-restore.ts → restorePageStateFromStorage)
页面 `useDidShow` 触发后:
1. `ensurePageSocketReady()` 确保 WebSocket 连接
2. 如果 `fromModifyDestination=1` + 有相同终点 toast 标记 → 弹提示
3. 调用 `restorePageStateFromStorage()`:
   - 读取 storage 中的 `startSite/endSite/orderId/tripStatus`
   - 如果 `tripStatus` 有值且不等于 1 → **调用 `getOrderActive()` 接口**
   - 接口返回数据 → 用真实订单数据更新页面状态
   - ⚠️ 接口返回空 → **清缓存并 redirect 到首页**(模拟器环境下无真实订单会踩到此分支)
### 5.2 预览态恢复(use-modify-destination-flow.ts → useEffect)
依赖 `[tripStatus, orderId, endSite]` 变化后执行:
1. 读取缓存快照(session + candidate + override + routePoints)
2. 调用 `resolveFlowState` 推导状态:
   - session.active + orderId 匹配 + candidate 有效坐标 → `PREVIEWING`
   - session.active + 无 candidate → `SELECTING`
   - 其他 → `IDLE`
3. 如果 candidate 与 previousEndSite 相同 → 自动取消(触发 `onCancelReview`)
4. 否则 → `applyPreviewingState`:设置 `previewEndSite`,`isModifyDestinationReviewVisible = true`
### 5.3 预览路线请求(use-modify-destination-preview-route.ts)
`isModifyDestinationReviewVisible` 变为 true 后:
1. 确定起点:
   - 接驾中/车辆已到达 → 使用 `startSite`(上车点)
   - 行程中 → 使用 `vehicleLocation`(车辆实时位置)
2. 调用 `requestRouteAndFare` 请求新路线 + 费用估算
3. 启动 **60 秒重估价倒计时**:超时后用户再点确认需要先刷新报价
---
## 六、阶段四:确认修改(handleConfirmModifyDestinationWithGuard)
**文件**:`index.tsx` 第 1855-1896 行
### 前置校验(任一命中则中断):
1. socket status === 0 → 弹失败弹窗 + 退出预览态
2. 距离/时长阈值校验(`shouldBypassLocalLimit` 为 false 时)→ 弹失败弹窗
3. `pendingConfirmRequote`(60s 超时)→ 重新估价 + 要求二次确认
### 提交流程(handleConfirmModifyDestination):
1. 锁定按钮(`isConfirmingModifyDestination = true`)
2. 全局 loading:「正在修改目的地」
3. 调用 API:`POST /itinerary/api/v1/mini-program/order/orderChange`
   ```json
   {
     "endPointId": "新终点站点ID",
     "orderId": "订单ID",
     "selectedRouteId": "选中路线ID",
     "selectedRoutePlanRule": "路线规划策略",
     "navigationSnapshotId": "导航快照ID"
   }
   ```
4. **成功**:
   - 构建新终点 → 更新 storage + state
   - `exitModifyDestinationReview({ shouldRestorePreviousEndSite: false })`
   - showToast「行程已更新」
   - 触发 `onModifySuccess`:清空路线选择 + 重新拉取后端行程路线
5. **失败**:弹修改失败弹窗
### 确认按钮禁用条件:
- 提交请求中
- 修改次数查询中 / 未就绪
- 路线关键参数未就绪(routeID / routePlanRule / snapshotId)
- 剩余修改次数为 0
---
## 七、阶段五:取消/退出预览态
| 触发场景 | 来源 | 行为 |
|---------|------|------|
| 点击返回按钮 | `handleBack` | 恢复旧终点 + 旧路线 → 退出预览态 |
| socket 推送不可修改 | `effectiveModifyDestinationSocketStatus === 0` | 恢复 + 弹失败弹窗 |
| 新旧终点相同 | 恢复流程中自动检测 | 触发 `onCancelReview` 恢复 |
| 失败弹窗确认 | `handleAcknowledgeModifyDestinationFail` | 恢复旧路线 |
| 页面离开/销毁 | `beforeBackRef` | 清理所有修改目的地缓存 |
| 预览态内点"编辑" | `onEditDestination` | 重新执行 `handleStartModifyDestination`(跳回选点页) |
**`exitModifyDestinationReview` 核心逻辑**:
- 参数 `shouldRestorePreviousEndSite`(默认 true):从 session 取旧终点恢复给页面
- 清空所有内存 state(previewEndSite / previousEndSite / previousFareEstimate / previousRoutePoints)
- 清空所有 storage 缓存(session / candidate / override / routePoints)
- 清空本地终点覆盖(overrideEndSite)
- 流程状态 → `IDLE`
---
## 八、关键 Storage Keys 一览
| Key | 用途 | 生命周期 |
|-----|------|---------|
| `modifyDestinationSession` | 修改会话(active / previousEndSite / orderId) | 进入选点页 → 确认/取消后清除 |
| `modifyDestinationCandidate` | 候选新终点(选点页写入) | 选点后写入 → 预览态读取后消费 |
| `modifyDestinationRoutePoints` | 旧路线点快照 | 启动时写入 → 取消后用于回滚路线 |
| `modifyDestinationOverride` | 本地终点覆盖值 | 修改确认后写入 → 后端推送同步后清除 |
| `modifyDestinationSkipAcQueryOnReturn` | 返回时跳过空调查询 | 选点返回时写入 → 消费后清除 |
| `modifyDestinationSocketStatus` | socket 推送的修改资格状态 | 实时更新 |
| `modifyDestinationPendingSameSiteToast` | 新旧终点相同 toast 标记 | 选点时写入 → 返回后消费 |
---
## 九、涉及的核心文件
| 文件 | 职责 |
|------|------|
| `hooks/use-modify-destination-flow.ts` | 流程状态管理、缓存读写、预览态恢复/退出 |
| `hooks/use-modify-destination-preview-route.ts` | 预览路线请求 + 60s 重估价倒计时 |
| `hooks/use-page-restore.ts` | 页面恢复编排(含 `getOrderActive()` 守卫) |
| `hooks/use-view-delegates.tsx` | 视图层委托(修改目的地确认卡片渲染) |
| `components/modify-destination-review.tsx` | 预览确认 UI 卡片组件 |
| `components/modify-destination-fail-popup.tsx` | 修改失败弹窗 |
| `components/calling-car-modules/trip-action-bar.tsx` | 底部操作栏(修改目的地按钮) |
| `pages/site-search/index.tsx` | 站点搜索页(选终点入口之一) |
| `pages/select-map-site/hooks/use-select-map-site.ts` | 地图选点页(选终点入口之二) |
| `constants/modify-destination.ts` | 所有 storage key 常量 + `isSameModifyDestinationSite` 工具函数 |
| `api/begin-route.ts` | `orderChange` API(修改目的地后端接口) |
---
## 十、模拟器环境已知问题
**问题**:在 DebugOrderSimulatorPanel 模拟器中,订单走到送驾状态后点击「修改目的地」,选完终点返回行程页时会自动退出回首页。
**根因**:
1. 模拟器仅注入 socket 事件驱动前端状态,不会在后端创建真实订单
2. 选点页返回后 reLaunch 重建行程页 → `useDidShow` → `restorePageStateFromStorage`
3. 因为 storage 中 `tripStatus = 6`(行程中),满足查询条件,调用 `getOrderActive()` 接口
4. 接口查不到活跃订单(模拟器无真实订单)→ 返回 null
5. 代码认为「订单已结束」→ 清缓存 + `Taro.redirectTo('/pages/robotaxi/home/index')`
**代码位置**:`hooks/use-page-restore.ts` 第 288-297 行
**修复建议**:在 `syncSitesFromActiveOrder` 中增加对模拟订单 ID(`SIM-` 前缀)的豁免,跳过 `getOrderActive()` 调用。