# 小程序填写返程信息: 校验前置 mp 端 + 错误信息友好化(前端有 BUG 待排查) > **服务**: hl-mp-service (端口 8085) > **PR**: #1830 (已合并 dev, 待部署 prod) > **Issue**: #1828 (P0-紧急, 正式环境) > **日期**: 2026-05-07 > **影响范围**: 小程序「填写返程信息」页(自驾/飞机/火车 × 一起返程/分批返程 全部场景) > **@ 前端**: mmg --- ## ⚠️ 关键信息 正式环境用户在小程序「填写返程信息」点「确认提交」后弹 toast: > batchReplace.reqs[0].travelerIds: 出行人列表不能为空 用户 UI 上**已经勾选了「蒋桂龙」乘坐人员**,但后端收到的 `travelerIds` 是空 — 这是一个**双重 BUG**: 1. **前端 BUG(mmg 必修)**:hl-mini 小程序 `_buildTogetherPlan` 把勾选的 traveler 的 `id` 装进请求 body 时,因 ID 字段名漂移导致最终装入的是占位符 `order-N` 被过滤,致 travelerIds 空数组 2. **后端友好化(本 PR 修)**:`MpArrivalController.batchReplace` 用 `@Valid List<...>` 不递归到元素,校验穿透到 internal controller 才被 `@Validated` 触发,错误信息暴露内部参数名 `batchReplace.reqs[0]` --- ## 一、后端变更(本 PR 已合 dev) ### 1.1 接口契约 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 全量替换返程批次 | POST | `/mp/order/{orderId}/arrival/batch` | **错误信息变更(不破坏契约)** | body 形状不变;mp 端前置校验,错误信息中文友好 | ### 1.2 新错误码 | Code | Message | 触发条件 | |------|---------|----------| | `620001` | 请添加至少一批返程信息 | body 是空 List 或 null | | `620002` | 第 N 批返程信息:请选择至少一位乘坐人员 | body[N-1].travelerIds 为 null 或空 List | ### 1.3 旧错误信息(部署前)vs 新错误信息(部署后) | 场景 | 部署前 | 部署后 | |------|--------|--------| | body 空 | `batchReplace.reqs: 不能为空` | `请添加至少一批返程信息` | | 首批空 travelerIds | `batchReplace.reqs[0].travelerIds: 出行人列表不能为空` | `第 1 批返程信息:请选择至少一位乘坐人员` | | 第二批空 travelerIds | `batchReplace.reqs[1].travelerIds: 出行人列表不能为空` | `第 2 批返程信息:请选择至少一位乘坐人员` | ### 1.4 服务端日志增强 mp controller 抛出校验失败前会 log.warn: ``` mp/arrival/batch 校验失败 orderId={} userId={} bodySize={} 元素摘要={} ``` (元素摘要含 direction / transportType / travelerIds.size 协助排查前端实际发出 body) 部署后 mmg 可以让运维拉 hl-mp-service WARN 日志看真实 body 形状(每元素含 direction/transportType/travelerIds 是否为空)。 --- ## 二、前端待处理(mmg 必修) ### 2.1 嫌疑代码位置 `hl-mini/packages/trip/arrival/arrival.js`: **行 88(`_loadPassengers`)**: ```js id: String(t.orderTravelerId || t.travelerId || t.id || `order-${idx}`) ``` **行 681-686(`_buildTogetherPlan`)**: ```js const selected = this.data.passengers.filter(p => p.checked && !p._isLocal); const travelerIds = selected .map(p => String(p.id || '')) .filter(id => id && !id.startsWith('order-') && !id.startsWith('local-')); ``` ### 2.2 排查步骤(建议) 1. 在 `_buildTogetherPlan` 进入时打日志: ```js console.log('[arrival] passengers=', this.data.passengers, 'selected=', selected, 'travelerIds=', travelerIds); ``` 2. 复现「蒋桂龙」场景(正式订单),观察 passenger 对象的 `id` 字段真实值: - **如果是 `order-0` 这种占位符** → `_loadPassengers` 三个候选字段名(`orderTravelerId / travelerId / id`)都没拿到值,对照后端 `getOrderDetail` 接口返回的 `travelers[]` 元素**真实字段名**与三候选是否对齐 - **如果是雪花 ID** → `_buildTogetherPlan` 的 filter 逻辑是否有别的过滤条件把它排掉 3. 对照后端 VO(`hl-mp-service` 的 `MpOrderTravelerVO`)确认实际字段名 — 必要时让我贴出来 ### 2.3 separate 模式(多批次返程) `arrival.js` L711 `_buildSeparatePlans` 取 `b.travelerIds || []`,L717 已有"第 N 批缺少出行人"前置 toast 拦截 — 理论上不会发到后端。但若有路径绕过此 toast,本次后端校验是兜底。 --- ## 三、数据库行为 只读校验,不写库。校验失败立即抛 BusinessException 不进入 Feign 调 order-v2。 --- ## 四、边界行为 - body=null → 620001 "请添加至少一批返程信息" - body=[] → 620001 - body=[{travelerIds:null}] → 620002 "第 1 批..." - body=[{travelerIds:[]}] → 620002 - body=[{travelerIds:[10001]}, {travelerIds:[]}] → 620002 "第 2 批..." - body=[{travelerIds:[10001]}, {travelerIds:[10002]}] → 走 Feign 到 internal,正常处理 --- ## 五、不影响范围 - 仅 `POST /mp/order/{orderId}/arrival/batch` 一个接口 - mp create / update / delete / list 接口 0 改动 - internal controller `@Validated` 兜底仍在,admin / 其他客户端调 internal 仍受校验 - API 契约(body 形状)不变,前端无需改请求结构 --- ## 六、测试 - 单测: `MpArrivalControllerTest` **9/9 全绿** (5 原有 + 4 新增反例:空 body / null body / 首批空 travelerIds / 第二批空 travelerIds) - 测试服 round-trip: 待 dev 部署测试服后由 QA 验证 - prod 部署: 由运维管理员排期 (本 PR 不动 prod) --- ## 七、相关文档 - 关联 Issue: [wx/HL#1828](https://git.1814.love:8443/wx/HL/issues/1828) - 关联 PR: [wx/HL#1830](https://git.1814.love:8443/wx/HL/pulls/1830)