docs: PR #1830 mp 返程批量校验前置友好化 + 前端排查指引 (Closes wx/HL#1828)

这个提交包含在:
API Changelog Bot 2026-05-07 17:50:12 +08:00
父节点 fe9064da2f
当前提交 87b07e839e

查看文件

@ -0,0 +1,133 @@
# 小程序填写返程信息: 校验前置 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. **前端 BUGmmg 必修)**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)