From 48e0fb4e75647c313805a21d5c5db78be30bd016 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Tue, 22 Sep 2026 09:52:12 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20=E5=9B=A2=E6=9C=9F=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=88=87=E8=B5=B0=E5=90=8E=E9=87=8D=E6=94=BE?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=20groupBatchId=20=E5=A1=8C=E7=BC=A9=E4=B8=BA?= =?UTF-8?q?=E7=A9=BA=E4=B8=B2=EF=BC=88=E5=89=8D=E7=AB=AF=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E4=BA=A4=E6=8E=A5=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 管理后台 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) --- ...µ°后重放请求groupBatchId塌缩-前端缺陷-管理后台.md | 549 ++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 changelogs-v2/2026-09/22_frontend_团期详情页切走后重放请求groupBatchId塌缩-前端缺陷-管理后台.md diff --git a/changelogs-v2/2026-09/22_frontend_团期详情页切走后重放请求groupBatchId塌缩-前端缺陷-管理后台.md b/changelogs-v2/2026-09/22_frontend_团期详情页切走后重放请求groupBatchId塌缩-前端缺陷-管理后台.md new file mode 100644 index 00000000..d97788b5 --- /dev/null +++ b/changelogs-v2/2026-09/22_frontend_团期详情页切走后重放请求groupBatchId塌缩-前端缺陷-管理后台.md @@ -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