docs(changelog): 团期详情页切走后重放请求 groupBatchId 塌缩为空串(前端缺陷交接件)
changelog-filename-gate / validate (push) Failing after 2s

管理后台 test.1814.love:9443/notification/my-messages 弹三条报错:
「参数 groupBatchId 格式错误」×2 + 「接口不存在: GET /v3/admin/order/group-batch/requirement/hotel-households」。

根因在 hl-ui(mmg 侧):order-v2/batch/detail/index.vue 用 computed 从 route.params.code
反应式推导团期 id,该页 keep-alive;切到消息中心后 params.code 变 undefined,id 塌缩成空串,
RoomSummarySection / RoomHouseholdsSection / GroupVehicleRequirementSection 三个 watcher
无空值守卫,各自重放一次请求。同页 DisbandBanner 有 if (!props.groupBatchId) return 守卫,
全程 200,是页面内的阳性对照。

后端契约无变化:groupBatchId 是路径段,空串导致路径少一段,因此第三条落到路由未匹配的
「接口不存在」而非参数校验。本件只交接前端修法与三个端点的真实契约,不含后端改动。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-22 09:52:30 +08:00
共同撰写人 Claude Opus 5
父节点 9b0ecadfbd
当前提交 48e0fb4e75
@@ -0,0 +1,549 @@
---
schema: hl-changelog/v2
ticket: "frontend"
title: "团期详情页切走后重放请求导致 groupBatchId 塌缩(前端侧修复)"
consumer: admin
author: "wx(GIT)"
change_type: "前端缺陷"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
updated_at: "2026-09-22"
base: dev-v3
status_note: "2026-09-22 测试服 test.1814.love:9443 实测:账号 admin/团期管理员从团期详情页切换到 /notification/my-messages 后弹出三条报错(两条 400『参数 groupBatchId 格式错误』+ 一条 404『接口不存在』)。order-v3 日志(8186/8086 两实例,09:06:44/50)显示 MethodArgumentTypeMismatchException 收到 value=requirement-summary/vehicle-requirement,NoHandlerFoundException 命中 GET /v3/admin/order/group-batch/requirement/hotel-households;同一时段 09:06:48 带真实 id 的请求全部成功,hl-gateway 两实例 0 条路由未命中记录。根因是前端 order-v2/batch/detail/index.vue 用 computed(() => decodeURIComponent(String(route.params.code || ''))) 反应式推导 id,该页 keep-alive 缓存,切到消息中心(meta keepAlive/hidden/noTabs)后 route.params.code 变 undefined,id 求值为空串;『查看需求』Tab 下 RoomSummarySection/RoomHouseholdsSection/GroupVehicleRequirementSection 三个 watcher 缺空值守卫,重放请求把空串拼进本该是路径参数的位置,导致两个后缀字面量落进 GET /v3/admin/order/group-batch/{groupBatchId}(A2 团期详情)的 groupBatchId 位,另一个因少了中间段直接 404。同页 DisbandBanner 组件已有判空守卫,调用全程 200,是本页面内的阳性对照。三条后端接口路径/参数/权限码均未变更、已在测试服部署并正常工作;既有交接件 10_7316/15_7441/20_8046 记录的实现与本次核对一致。建议修法:三个子组件 watcher 照 DisbandBanner 写法补判空;或详情页不用 useRoute() 反应式推导 code,改挂载时快照/onDeactivated 停 watcher。同页 confirm-check/orders/status-logs 三个 Tab 本次未复现报错,是否另有 active 门控,列为待排查项。"
---
# 团期详情页切走后重放请求导致 groupBatchId 塌缩(前端侧修复)
## ⚠️ 关键变化
- **不是后端问题**:三条报错全部由前端在 id 为空串时仍发起请求触发;本次涉及的三条后端接口路径、参数、权限码均未变更,已在测试服部署并实测正常。
- **触发条件**:团期详情页 `order-v2/batch/detail/index.vue` 是 keep-alive 缓存页,从该页切换到 `/notification/my-messages`(路由 meta `keepAlive/hidden/noTabs`)后,详情页仍存活在缓存中,其 `computed(() => decodeURIComponent(String(route.params.code || '')))` 推导出的 id 因 `route.params.code` 变为 `undefined` 而塌缩成**空字符串**。
- **传导机制**:「查看需求」Tab 下 `RoomSummarySection`、`RoomHouseholdsSection`、`GroupVehicleRequirementSection` 三个组件的请求 watcher 缺空值守卫,空串重放请求后,URL 路径段整体消失,后面的字面量后缀滑进了 `groupBatchId` 应该在的位置,命中了两种不同的服务端拒绝形态(400 参数类型错误 / 404 无映射),而不是权限或路由错误。
- **正对照**:同页 `DisbandBanner` 组件已有 `if (!props.groupBatchId) return;` 守卫,全程请求 200,可作为直接抄的修复模板。
## 一、背景
### 现象(2026-09-22 测试服 test.1814.love:9443,账号 admin / 角色 团期管理员)
用户从团期详情页 `/order-v2/batch/detail/2100856430494973953` 切换到 `/notification/my-messages` 后,页面弹出三条报错:
- `参数 groupBatchId 格式错误,请检查后重试` × 2
- `接口不存在: GET /v3/admin/order/group-batch/requirement/hotel-households`
### 后端实测日志(order-v3,`hl-order-service-v3.log`,09:06:44 与 09:06:50 各一波,分别落在 8186 / 8086 两实例)
```
WARN GlobalExceptionHandler - 参数类型错误: param=groupBatchId, value=requirement-summary, requiredType=Long
WARN GlobalExceptionHandler - 参数类型错误: param=groupBatchId, value=vehicle-requirement, requiredType=Long
WARN org.springframework.web.servlet.PageNotFound - No mapping for GET /v3/admin/order/group-batch/requirement/hotel-households
WARN GlobalExceptionHandler - No handler found: GET /v3/admin/order/group-batch/requirement/hotel-households
```
同一时段 09:06:48,带真实 id(`groupBatchId=2100856430494973953`)的请求全部成功;hl-gateway 两实例 0 条路由未命中记录——请求确实到达了 order-v3,并被服务端正确拒绝(不是网关/路由问题)。
### 「表面看起来像」vs「实际是」
| 表面报错 | 看起来像 | 实际是 |
|---|---|---|
| `参数 groupBatchId 格式错误,请检查后重试` | 接口参数校验变严了 / 接口签名变了 | 前端把空串拼进了 URL,字面量后缀(`requirement-summary`/`vehicle-requirement`)落进了 `groupBatchId` 位,被 `GroupBatchQueryController` 的团期详情端点 `GET /v3/admin/order/group-batch/{groupBatchId}` 接住,Long 转换失败 |
| `接口不存在: GET /v3/admin/order/group-batch/requirement/hotel-households` | 接口被删了 / 路由配错了 | 空串导致中间路径段整体消失,剩下两段字面量拼在一起,天然不匹配任何 `@GetMapping`,属于合法的 404,接口本体从未变化 |
### 根因链路(前端产物级实测,测试服 `/var/www/hl-admin/assets`,2026-09-22 06:28 部署)
1. `order-v2/batch/detail/index.vue` 的 id 取值是反应式推导:`computed(() => decodeURIComponent(String(route.params.code || '')))`。
2. 该页是 keep-alive 缓存页;`/notification/my-messages` 路由 meta 为 `keepAlive/hidden/noTabs`,切过去后详情页仍在缓存中存活,而 `route.params.code` 已变 `undefined` → id 求值为空串。
3. 「查看需求」Tab 下三个子组件的 watcher 没有空值守卫,于是各重放一次请求:
- `RoomSummarySection`:`watch([active, groupBatchId], ...)` 只挡了 `active`,不挡空 id;
- `RoomHouseholdsSection`:同上;
- `GroupVehicleRequirementSection`:`watch(() => props.groupBatchId, ..., {immediate: true})`,无任何守卫。
4. 空串导致 URL 路径段整体消失,后缀滑进了 id 位置:
- `/v3/admin/order/group-batch/requirement-summary` → 被团期详情端点 `GET /v3/admin/order/group-batch/{groupBatchId}` 匹配,`{groupBatchId}` 位拿到字符串 `requirement-summary` → Long 转换失败;
- `/v3/admin/order/group-batch/vehicle-requirement` → 同上;
- `/v3/admin/order/group-batch/requirement/hotel-households` → 两段字面量后缀,中间没有 `{groupBatchId}`,不匹配任何映射 → 404。
5. **阳性对照(同一页面内)**:`DisbandBanner` 组件已有 `if (!props.groupBatchId) return;` 守卫,所以它调的 `approvals/page` 全程 200、没有报错。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 复用不改 | 契约未变,本次仅确认前端调用方式 |
| 2 | 团期子订单订房记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` | 复用不改 | 契约未变,本次仅确认前端调用方式 |
| 3 | 读团期正式用车需求 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 复用不改 | 契约未变,本次仅确认前端调用方式 |
## 三、接口详情
### 1. 全团需求汇总
`GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`
**VO**: 无独立请求体(仅路径参数)→ `GroupRequirementSummaryRespVO`
#### 使用场景
团期详情页「查看需求」Tab 的「用房 · 汇总」板块,展示全团逐日 × 酒店 × 房型的用房间数合计、大巴座位合计、各子订单特殊需求标签。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| groupBatchId | Path | Long | 是 | 团期 ID(雪花 id)。**本次报错的病灶**:该字段取自路由 `code` 参数,页面切走后再重放请求时其反应式取值会塌缩为空串,导致这个路径段被 URL 后面的字面量后缀顶替 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| activeOrderCount | int | 在团子订单数(仅排除 CANCELLED,含 COMPLETED) |
| hotelNeededOrderCount | int | 需要订房的户数(needsHotel=true 并上已提交有效用房需求的户) |
| hotelSubmittedOrderCount | int | 已提交有效用房需求(非打回态)且计入 dailyRoomBreakdown 的户数 |
| vehicleRequirementCount | int | 已提交用车需求的子订单数 |
| dailyRoomBreakdown[].dayNumber | int | 行程天数(从 1 开始) |
| dailyRoomBreakdown[].stayDate | LocalDate | 该晚住宿日期;团期无出发日时为 null |
| dailyRoomBreakdown[].hotels[].hotelId | Long(字符串序列化) | 酒店 ID;无候选或未填酒店时为 null |
| dailyRoomBreakdown[].hotels[].rooms[].roomCategory | String | 房型大类编码 |
| dailyRoomBreakdown[].hotels[].rooms[].totalRoomCount | int | 该天该酒店该房型合计间数 |
| vehicleSeatSummary[].vehicleType | String | 车型大类编码(suv/mpv/bus/sedan) |
| vehicleSeatSummary[].totalSeats | int | 合计座位数 |
| orderSpecialTags[].orderId | Long | 子订单 ID |
| orderSpecialTags[].specialTags | String[] | 特殊需求标签列表 |
#### 请求示例
```
GET /v3/admin/order/group-batch/2100856430494973953/requirement-summary
Authorization: Bearer {token}
```
无请求体。
#### 响应示例
(按 VO 字段契约构造,非测试服抓包实测)
```json
{
"code": 0,
"message": "success",
"data": {
"activeOrderCount": 12,
"hotelRequirementCount": 10,
"hotelNeededOrderCount": 10,
"hotelFlagMismatchOrderCount": 0,
"hotelSubmittedOrderCount": 9,
"vehicleRequirementCount": 8,
"dailyRoomBreakdown": [
{
"dayNumber": 1,
"stayDate": "2026-09-12",
"rooms": [{"roomCategory": "STANDARD", "roomCategoryName": "标间", "totalRoomCount": 5}],
"hotels": [{"hotelId": "2023714929877450753", "hotelName": "海堂酒店", "totalRoomCount": 5, "rooms": [{"roomCategory": "STANDARD", "roomCategoryName": "标间", "totalRoomCount": 5}]}]
}
],
"vehicleSeatSummary": [{"vehicleType": "bus", "vehicleTypeName": "35座大巴", "totalSeats": 35, "totalCount": 1}],
"orderSpecialTags": [{"orderId": 2101506167043985410, "requirementType": "HOTEL", "specialTags": ["连通房"]}]
}
}
```
#### 空数据 / 降级响应
各汇总数组在无对应需求时返回空数组,不会返回 null;`hotelName`/`vehicleTypeName` 在资源服务不可用或字典缺失时可能为 null,间数照常返回。
#### 错误响应
groupBatchId 路径段为空串时的实际报错(本次事故复现的原样响应):
```json
{"code": 400, "message": "参数 groupBatchId 格式错误,请检查后重试", "data": null, "success": false}
```
团期不存在:
```json
{"code": 589500, "message": "团期不存在", "data": null, "success": false}
```
当前角色未持 `group-batch:view` 权限,或该团期不在本人名下:
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "success": false}
```
#### 业务边界
- `groupBatchId` 必须是真实存在的 Long 型团期 id;空串、`undefined` 字面量、非数字字符串都会被当成非法值处理,统一走 400,不会得到空数据。
- 需要角色权限码 `group-batch:view`;无权限或团期不在名下统一报 589507(不区分两种子原因)。
### 2. 团期子订单订房记录
`GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households`
**VO**: 无独立请求体(仅路径参数)→ `GroupHotelHouseholdsRespVO`
#### 使用场景
团期详情页「查看需求 · 用房」板块的下半区,按子订单(户)展示订房记录:每户一张卡(团号/联系人/人数/定制师/状态/配房需求)+ 逐晚填报明细(住宿日期/酒店/城市/房型/间数)。与「全团需求汇总」是同一板块的上下两块,各走各的接口。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| groupBatchId | Path | Long | 是 | 团期 ID(雪花 id)。同上,是本次报错的病灶字段 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| groupBatchId | Long(字符串序列化) | 团期 ID |
| departDate | LocalDate | 团期出发日期;未定出发日时为 null,此时各晚 stayDate 也全为 null |
| householdCount | int | 本列表的户数(=需订房户数),可能大于 countedHouseholdCount,差值是正被打回的户 |
| countedHouseholdCount | int | 其中计入上方汇总间数的户数 |
| households[].orderId | Long(字符串序列化) | 子订单 ID |
| households[].orderNo | String | 子订单团号 |
| households[].status | String | 需求状态编码:PENDING_REVIEW/PENDING/PROCESSING/DONE/REJECTED_TO_CONSULTANT/REJECTED_TO_ADMIN;该户尚未提交需求时为 null |
| households[].countedInSummary | boolean | 该户是否计入了「用房汇总」的间数;两个打回态为 false |
| households[].days[].dayNumber | int | 第几晚(从 1 起) |
| households[].days[].customerSelfBooked | boolean | 该晚是否客户自订;自订晚 hotels 恒为空且不计入汇总间数 |
| households[].days[].hotels[].hotelId | Long(字符串序列化) | 酒店 ID;无候选或未填酒店时为 null |
| households[].days[].hotels[].district | String | 酒店所在区县中文名,页面展示应优先用本字段而非 city(同团期酒店 city 常同为一个地级市) |
| households[].days[].hotels[].rooms[].roomCount | int | 间数 |
#### 请求示例
```
GET /v3/admin/order/group-batch/2100856430494973953/requirement/hotel-households
Authorization: Bearer {token}
```
无请求体。
#### 响应示例
(按 VO 字段契约构造,非测试服抓包实测)
```json
{
"code": 0,
"message": "success",
"data": {
"groupBatchId": "2100856430494973953",
"departDate": "2026-09-12",
"householdCount": 10,
"countedHouseholdCount": 9,
"households": [
{
"orderId": "2101506167043985410",
"orderNo": "GT-26-0081",
"customerName": "张三",
"participantCount": 3,
"consultantId": "10086",
"consultantName": "李定制",
"status": "DONE",
"statusName": "已完成",
"countedInSummary": true,
"remark": "希望安排有窗房间",
"specialTags": ["连通房"],
"returnRemark": null,
"returnedAt": null,
"days": [
{
"dayNumber": 1,
"stayDate": "2026-09-12",
"customerSelfBooked": false,
"hotels": [
{
"hotelId": "3001000000000000005",
"hotelName": "海堂酒店",
"city": "呼伦贝尔市",
"district": "海拉尔区",
"totalRoomCount": 2,
"rooms": [{"roomTypeId": "1001", "roomTypeName": "亲子房", "roomCategory": "PARENT_CHILD", "roomCategoryName": "亲子房", "roomCount": 2}]
}
]
}
]
}
]
}
}
```
#### 空数据 / 降级响应
该户尚未提交需求时 `status` 为 null、`days` 为空列表(不是缺少该户,仍会出现在 `households` 中,仅 `countedInSummary=false`)。
#### 错误响应
groupBatchId 路径段整体消失时的实际报错(本次事故复现的原样响应,两段字面量拼接后不匹配任何映射):
```json
{"code": 404, "message": "接口不存在: GET /v3/admin/order/group-batch/requirement/hotel-households", "data": null, "success": false}
```
团期不存在:
```json
{"code": 589500, "message": "团期不存在", "data": null, "success": false}
```
无权限:
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "success": false}
```
#### 业务边界
- 与「全团需求汇总」判权同码:`group-batch:view`。
- `groupBatchId` 路径段一旦缺失(不是格式错,而是整段消失),会命中 404 而不是 400——这与另外两个端点的失败形态不同,是「哪个路径段消失」决定的,不是接口行为不一致。
### 3. 读团期正式用车需求
`GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: 无独立请求体(仅路径参数)→ `GroupVehicleRequirementRespVO`
#### 使用场景
团期详情页「查看需求 · 用车」板块,展示团期管理员已确认/正在编辑的正式用车需求(乘车分组、逐日人数、审核留痕),以及配车计划刷新的只读观测状态。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| groupBatchId | Path | Long | 是 | 团期 ID(雪花 id)。同上,是本次报错的病灶字段 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| requirementId | Long(字符串序列化) | 正式需求主键 |
| groupBatchId | Long(字符串序列化) | 团期聚合主键 |
| status | String | DRAFT/CONFIRMED/DISPATCHED/DONE/PENDING_RECONFIRM/CANCELLED |
| version | Integer | 版本号(下次提交须回传做乐观锁) |
| remark | String | 整份备注;撤回/免车会把那次操作追加进来,不覆盖原备注 |
| confirmedBy | String | 整份确认人;DRAFT 时为 null |
| confirmedAt | LocalDateTime | 整份确认时间;DRAFT 时为 null |
| planRefreshState | String | 配车刷新状态原值:null=从未登记过刷新(多数团期正常态)/PENDING/DONE/FAILED |
| planRefreshStalled | Boolean | 刷新是否已停滞、不会自愈(恒非 null);true=必须有人处置 |
| planRefreshStalledReason | String | 停滞归因:STATE_FAILED/COMMAND_FAILED/TIMEOUT;未停滞为 null |
| planRefreshReplayExhausted | Boolean | 人工重投额度是否已耗尽(恒非 null) |
| groups[].groupCode | String | 分组键,直接作为车费 alloc_group |
| groups[].days[].headcount | Integer | 该组该日用车人数(乘车人数,非户数) |
| groups[].days[].memberOrderCount | Integer | 当日成员户数,供填人数时对照 |
#### 请求示例
```
GET /v3/admin/order/group-batch/2100856430494973953/vehicle-requirement
Authorization: Bearer {token}
```
无请求体。
#### 响应示例
(按 VO 字段契约构造,非测试服抓包实测)
```json
{
"code": 0,
"message": "success",
"data": {
"requirementId": "1867000000101",
"groupBatchId": "2100856430494973953",
"status": "CONFIRMED",
"version": 3,
"remark": "全程 33 座",
"confirmedBy": "10086",
"confirmedAt": "2026-09-14T10:30:00",
"planRefreshState": null,
"planRefreshReplayCount": null,
"blockedStage": null,
"planRefreshStalled": false,
"planRefreshStalledReason": null,
"planRefreshTimeoutAt": null,
"planRefreshReplayExhausted": false,
"groups": [
{
"groupId": "1867000000009",
"groupCode": "BUS",
"vehicleType": "35座大巴",
"serviceStartDate": "2026-09-12",
"serviceEndDate": "2026-09-16",
"days": [{"tripDate": "2026-09-12", "headcount": 9, "memberOrderIds": ["2101506167043985410"], "memberOrderCount": 3}]
}
]
}
}
```
#### 空数据 / 降级响应
该团期尚未形成正式需求时接口返回 `data = null`(`GroupBatchRequirementController` 方法 javadoc 原文:「读团期正式用车需求(未形成时返回 null;带配车刷新状态只读投影)」),不是报错——编辑页首次打开就是这个状态,前端应按空态渲染而不是当异常处理。
#### 错误响应
groupBatchId 路径段为空串时的实际报错(本次事故复现的原样响应):
```json
{"code": 400, "message": "参数 groupBatchId 格式错误,请检查后重试", "data": null, "success": false}
```
团期不存在:
```json
{"code": 589500, "message": "团期不存在", "data": null, "success": false}
```
无权限:
```json
{"code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "success": false}
```
#### 业务边界
- 判权码与前两个端点不同:本端点用 `group-batch:demand:confirm`,而不是 `group-batch:view`;三条接口不是同一权限码守卫的,前端如需按权限隐藏 Tab 内容,要分别判断。
- `status` 不是恒定值,`PUT`/`withdraw`/`waive` 各自会把它改成不同的值,前端不要假设「有数据就是 CONFIRMED」。
- `planRefreshStalled=true` 时应引导「联系后台排查」,`planRefreshReplayExhausted=true` 时再点确认只会收到 809210,与本次问题无关但同属该接口只读契约,一并列出供前端识别。
## 四、契约约束与正确调用方式
| 场景 | 结果 |
|---|---|
| ✅ 用当前团期真实数字 id 拼接 | `/v3/admin/order/group-batch/2100856430494973953/requirement-summary` → 200 |
| ❌ id 为空串,路径只少一段 | `/v3/admin/order/group-batch/requirement-summary` → 400『参数 groupBatchId 格式错误』,`requirement-summary` 落进了 `groupBatchId` 位 |
| ❌ id 为空串,路径中间少一段 | `/v3/admin/order/group-batch/requirement/hotel-households` → 404『接口不存在』,两段字面量直接拼接,不匹配任何映射 |
### 发起请求前的必要动作
前端在这三个组件的请求 watcher 里,发起请求前必须先判断 `groupBatchId` 是否为空/`undefined`,为空时直接 `return`,不要依赖 URL 层面的容错——同一路径下不同位置的空段,服务端表现不同(一种落成合法但类型错误的字符串触发 400,另一种导致整条路径不匹配触发 404),两种表现都不能被前端当作可忽略的静默失败来处理。keep-alive 缓存页在路由离开后仍存活,是触发这一问题的必要条件,仅靠「首次挂载时判空」不足以覆盖切走再切回的场景。
## 五、数据库行为
本次三条接口均为只读 GET,均无数据库写操作,无字段/索引/事务变更。
## 六、边界行为
- 未登录 → 401(网关拦截)。
- `groupBatchId` 路径段为空串/非数字字符串 → 400『参数 groupBatchId 格式错误,请检查后重试』。
- `groupBatchId` 路径段整体缺失,落地路径与任何 `@GetMapping` 都不匹配 → 404『接口不存在』。
- 团期不存在 → 589500。
- 当前角色未持对应权限码,或该团期不在本人名下 → 589507(三条接口分持 `group-batch:view` / `group-batch:view` / `group-batch:demand:confirm` 两种权限码,见各自「接口详情·业务边界」)。
- 该团期尚未形成正式用车需求时,`GET vehicle-requirement` 返回 `data=null`,不是 404/500。
## 六.5 枚举
### status(`GroupVehicleRequirementRespVO.status`)
**所属字段**: `data.status` | **类型**: `String`
| 值 | 说明 |
|---|---|
| DRAFT | 草稿;`PUT`/`withdraw` 后均落此值 |
| CONFIRMED | 已确认;`waive`(整团免车)后也落此值 |
| DISPATCHED | 已配车 |
| DONE | 已完成 |
| PENDING_RECONFIRM | 受控重开窗口内的中间态 |
| CANCELLED | 已取消 |
### status(`GroupHotelHouseholdsRespVO.HouseholdItem.status`,需求状态编码 RequirementStatus)
**所属字段**: `data.households[].status` | **类型**: `String`(该户尚未提交需求时为 null)
| 值 | 说明 |
|---|---|
| PENDING_REVIEW | 待审核 |
| PENDING | 待处理 |
| PROCESSING | 处理中 |
| DONE | 已完成 |
| REJECTED_TO_CONSULTANT | 打回定制师 |
| REJECTED_TO_ADMIN | 打回管理员 |
## 六.6 修改前后对比
本文档不涉及接口契约改造(三条端点均为「复用不改」),无字段级契约对比;以下是前端实现层面的行为级对比。
### 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 团期详情页切换到其他路由(如消息中心)后,详情页仍在 keep-alive 缓存中存活 | 「查看需求」Tab 下三个子组件的 watcher 用塌缩为空串的 groupBatchId 重放请求,弹出 3 条报错(2 个 400 + 1 个 404) | 请求前先判断 groupBatchId 是否为空,为空直接跳过 watcher 回调,不发起请求,不报错 |
## 六.7 影响评估
- 是否破坏向后兼容:否,接口契约本身未改动。
- 影响范围:仅限团期详情页「查看需求」Tab 下依赖反应式 `groupBatchId` 的组件,在 keep-alive 缓存页被切走后再次触发 watcher 的场景;正常打开、停留、直接操作详情页不受影响。
- 前端 workaround 清理点:无——这是需要新增的判空守卫,不是要撤销的旧代码。
## 七、不影响范围
- 后端代码:本次零改动。
- 数据库 schema:无变更。
- 网关路由:三条端点均已在既有 `/v3/admin/**` 通配路由内,本次事故期间 hl-gateway 两实例 0 条路由未命中记录。
- 团期详情页正常打开、停留、直接操作时的行为:不触发本问题,仅路由切走再重放的场景触发。
- `DisbandBanner` 等已有判空守卫组件的行为:不受影响,本次事故期间全程 200。
- 三条接口在非空 `groupBatchId` 场景下的既有契约与行为:未变化。
## 八、测试环境已验证
真实日志(✓ = 已实测):
```
2026-09-22 09:06:44 order-v3(8186) WARN GlobalExceptionHandler - 参数类型错误: param=groupBatchId, value=requirement-summary, requiredType=Long ✓
2026-09-22 09:06:44 order-v3(8186) WARN org.springframework.web.servlet.PageNotFound - No mapping for GET /v3/admin/order/group-batch/requirement/hotel-households ✓
2026-09-22 09:06:44 order-v3(8186) WARN GlobalExceptionHandler - No handler found: GET /v3/admin/order/group-batch/requirement/hotel-households ✓
2026-09-22 09:06:50 order-v3(8086) WARN GlobalExceptionHandler - 参数类型错误: param=groupBatchId, value=vehicle-requirement, requiredType=Long ✓
2026-09-22 09:06:48 同一时段带真实 groupBatchId 的请求全部成功 ✓
hl-gateway 两实例 0 条路由未命中记录 ✓
```
验证团期:`groupBatchId=2100856430494973953`。
前端产物级取证(测试服 `/var/www/hl-admin/assets`,2026-09-22 06:28 部署):
- `order-v2/batch/detail/index.vue` 的 id `computed` 反应式推导逻辑 ✓
- `/notification/my-messages` 路由 meta 为 `keepAlive/hidden/noTabs` ✓
- `RoomSummarySection`/`RoomHouseholdsSection`/`GroupVehicleRequirementSection` 三个 watcher 缺空值守卫 ✓
- `DisbandBanner` 组件已有 `if (!props.groupBatchId) return;` 守卫,同页阳性对照 ✓
源码引用(`origin/dev-v3`):
```
GroupBatchQueryController.java:108 GET /v3/admin/order/group-batch/{groupBatchId}(A2团期详情)存在,是空 id 被误路由到的落点 ✓
GroupBatchRequirementController.java:79-86 GET .../requirement-summary 端点存在,判权 group-batch:view ✓
GroupBatchRequirementController.java:102-112 GET .../requirement/hotel-households 端点存在,判权 group-batch:view ✓
GroupBatchRequirementController.java:219-226 GET .../vehicle-requirement 端点存在,判权 group-batch:demand:confirm ✓
GlobalExceptionHandler.java MethodArgumentTypeMismatchException → 400『参数%s格式错误』 ✓
GlobalExceptionHandler.java NoHandlerFoundException → 404『接口不存在』 ✓
GroupBatchErrorCode.java:15 589500 团期不存在 ✓
GroupBatchPermissionGuard.java:50 PERMISSION_VIEW = "group-batch:view" ✓
GroupBatchPermissionGuard.java:96 PERMISSION_DEMAND_CONFIRM = "group-batch:demand:confirm" ✓
GroupVehicleRequirementRespVO.java status 枚举、配车刷新观测块字段定义 ✓
GroupHotelHouseholdsRespVO.java households/days/hotels/rooms 字段结构 ✓
GroupRequirementSummaryRespVO.java dailyRoomBreakdown/vehicleSeatSummary/orderSpecialTags 字段结构 ✓
```
【需确认】未取证项:同页 `confirm-check`/`orders`/`status-logs` 三个 Tab 本次未复现报错,是否另有 `active` 门控或取值路径不同,属前端待排查项,本文档未验证,不作为确认结论。
## 十、相关文档
- 全团需求汇总完整历史契约:`changelogs-v2/2026-09/10_7316_团期全团需求汇总补权限码与在团口径-结算汇总已核单数纠正-修改接口-管理后台.md`
- 团期正式用车需求完整历史契约:`changelogs-v2/2026-09/15_7441_团期正式用车需求声明-新增接口-管理后台.md`
- 团期子订单订房记录完整历史契约:`changelogs-v2/2026-09/20_8046_团期用房新增子订单订房记录接口-新增接口-管理后台.md`
## 关联 / 联系人
### 链接
- Issue:无(纯前端缺陷,无后端工单号)
- PR:无(本次零后端代码改动,未产生新 PR)
### 联系人
- **后端负责人**: @wx