文件
hl-api-changelog/changelogs-v2/2026-09/23_frontend_用房汇总首屏必失败同页两组件共用同一接口被去重取消-前端缺陷-管理后台.md
T

9.6 KiB
原始文件 Blame 文件历史

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 frontend 「用房 · 汇总」首屏必现「汇总加载失败」:同页两个组件调同一个接口,后发的把先发的 abort 了 admin wx(GIT) 前端缺陷 not_required not_required verified mmg 4b33ccbd9af619723a9c58373531627cb6e31e9b v2.1 2026-09-23 2026-09-23 wx 反馈团期详情「查看需求」Tab 的「用房 · 汇总」显示「汇总加载失败,可点右上角「刷新」重试」,同页「用房 · 子订单订房记录」「用车 · 汇总」正常。根因在 hl-ui:RoomSummarySection 与 VehicleSummarySection 在同一 tick 各调一次 getGroupRequirementSummary(同 method+url+params+data),而该封装未传 cancelDuplicate:false,request.js 的去重拦截器用后发的 AbortController abort 掉先发的那条;渲染顺序 Room 在前,所以被取消的恒是 Room,catch{} 把 cancel 当失败吞掉、summary 留 null、落到空态文案。后端与数据均正常:测试服 nginx 09-21~09-23 该团期 16 次 requirement-summary 全部 HTTP 200、响应体与应用日志确认成功的载荷等长,order-v3 应用日志 09-23 两次打出「全团需求汇总完成 inGroupOrders=2 hotelNeeded=2 hotelCounted=1」,09-22 18:55 起零 ERROR;DB 侧两户 needs_hotel=1、一户有 active 需求行,与页面「共 2 户 · 计入 1 户」吻合。本条无后端改动。 | 2026-09-23 mmg 交付:requirement-summary 收归 RequirementTab 统一取数只调一次,两汇总区块改 props 下发(失败态只看 error prop),catch 护栏区分 ERR_CANCELED(去重 abort/路由切换)不落假失败态,空 id/序号守卫随取数上移父级;3 spec 31 例锁单次取数/取消护栏/失败态 2026-09-23 dev-v3

「用房 · 汇总」首屏必现加载失败:请求去重把它自己的请求取消了

服务: hl-order-service-v3(后端零改动,后端与数据均正常) 页面: 管理后台 → 团期订单 → 团期详情 → 「查看需求」Tab → 「用房 · 汇总」区块 接口: GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary 日期: 2026-09-23 影响范围: 仅前端。所有团期、所有用户、每次首次进入该 Tab 必现;点「刷新」即恢复


一、现象

「用房 · 汇总」渲染成空态:汇总加载失败,可点右上角「刷新」重试。 同一页的「用房 · 子订单订房记录」「用车 · 汇总」「用车 · 子订单需求记录」全部正常。 点该区块右上角「刷新」立刻就好。


二、根因(对 origin/v2.1 = 61a549bd 源码实读)

三个事实叠起来构成这个缺陷:

① 同一个接口被同页两个组件各调一次,且在同一 tick

文件 行 调用
src/views/order-v2/batch/detail/components/RoomSummarySection.vue :74 :169 import { getGroupRequirementSummary } → await getGroupRequirementSummary(batchId)
src/views/order-v2/batch/detail/components/VehicleSummarySection.vue :94 :152 同上

两者都在 watch([active, groupBatchId], …, { immediate: true }) 里触发(RoomSummarySection.vue:180-190、VehicleSummarySection.vue:163-173),active 同源,必定同 tick。 RequirementTab.vue 的渲染顺序是 RoomSummarySection 在前、VehicleSummarySection 在后。

② 这个 API 封装没关掉去重

src/api/orderV2GroupBatch.js:563-568:

export function getGroupRequirementSummary(groupBatchId, config = {}) {
  return http.get(`${BASE}/${String(groupBatchId)}/requirement-summary`, null, {
    ...V3,
    ...config,
  })
}

同一个文件里 confirm(:507)、reject(:528) 以及另外十余个封装都带着 cancelDuplicate: false,只有这个没带。

③ 去重拦截器对同 key 的后发请求,是 abort 掉先发的那条

src/utils/request.js:260-275:

if (config.cancelDuplicate === false) { …不入 pending 列表… }
…
oldController.abort('取消重复请求')
const controller = new AbortController()

key 的构成(:235-237)是 [method, url, stableStringify(params), stableStringify(data)].join('&')。 本接口无 query、无 body,两个组件算出的 key 逐字符相同。

⇒ 后发的 Vehicle 把先发的 Room abort 了。 Room 那条在 adapter 发出前就被取消,所以网络层根本看不到它(测试服 nginx 每次页面加载只落一条 requirement-summary,与此吻合)。

④ 取消被当成了失败

RoomSummarySection.vue:169-175:

try { const res = await getGroupRequirementSummary(batchId); … } catch { }

裸 catch {} 不区分 axios cancel 与真实失败,summary 留 null,模板落到 :55 的空态文案。 点「刷新」时只有一条请求在途、没人取消它,所以必然成功——这就是「刷新即好」的由来。


三、后端与数据侧的核查结果(均正常,供排除用)

  • nginx 访问日志(测试服 access.log{,.1,.2.gz}):该团期 09-21~09-23 共 16 次 requirement-summary,全部 HTTP 200;响应体 1116 B(09-22 13:04 上 transferSummary 之前)/ 1358 B(之后)。对照组:前端在路由参数未就绪时发的空 id 请求 group-batch//requirement-summary 只有 132 B,说明 1358 B 是完整成功载荷、不是 code≠0 的 Result。
  • order-v3 应用日志:2026-09-23 09:09:45.822 / 09:11:01.395 两次 全团需求汇总完成 groupBatchId=2100856430494973953 inGroupOrders=2 hotelReqs=1 hotelNeeded=2 hotelCounted=1 vehicleReqs=1 transferReqs=1;09-22 18:55 起 order-v3 零 ERROR。
  • 数据:两个子订单 needs_hotel=1;HL20260918155619496 有 is_active=1 的 PENDING_REVIEW 用房需求(两晚、JSON 合法、special_tags=["禁烟"]),HL20260922161158291 无用房需求行(flow_status=AWAITING_PROFILE)。与页面「共 2 户 · 计入汇总 1 户」逐项吻合,无脏数据。
  • 服务端代码:GroupBatchRequirementService.summary() 链路上 filterCountedHotelRequirements 对无需求行的户只是不入集合(不会 NPE),aggregateDailyRooms / parseServiceDates / aggregateVehicleSeats / collectSpecialTags 均 try/catch 降级,酒店名与车型名的 Feign/字典加载失败一律降级为空 Map。无 null-guard 缺口。

四、修复建议

主修(结构):由 RequirementTab 只请求一次 requirement-summary,经 props 下发给 RoomSummarySection / VehicleSummarySection。父组件已持有两者的 ref,改动面可控;这样同一接口一次页面加载只打一次,顺带省掉一次重复请求。

护栏(更重要,建议与主修一起做):两个 Summary 组件的 catch {} 要区分「被取消」与「真失败」,被取消时不要落成「加载失败」态:

} catch (e) {
  if (axios.isCancel?.(e) || e?.code === 'ERR_CANCELED') return   // 保持 loading / 旧数据,不置失败态
  // 真实失败才走原逻辑
}

src/utils/request.js:596 已经有这个判别写法可以直接抄。之所以说它比主修更重要:request.js:315 在路由切换时也会 abort('路由切换,取消请求'),同样会落进这个 catch;而且只要将来任何组件再复用同一个接口,缺陷就会原样复现。这条护栏把「一次修好」变成「不会再犯」。

一行止血(如果要先快速恢复):给 getGroupRequirementSummary 的配置加上 cancelDuplicate: false,与同文件其余封装写法一致。代价是同一页会真的发两次相同请求。

⚠️ 一个容易踩空的点

request.js 里有两个名字相近、行为不同的开关,别改错:

开关 位置 对本缺陷
dedupe(时间窗内重复请求直接拒) :173-208 对 GET 默认就不生效——:183 写着 methodUpper === 'GET' && config.dedupe !== true 才进这段。设 dedupe: false 改变不了任何事
cancelDuplicate(同 key 后发 abort 先发) :260-275 就是它,对 GET 生效

五、业务边界

  • 不限于某一个团期。机制是同 tick 双请求、后者必取消前者,与团期数据无关;09-22 14:37 团期 2101506167098511362 的访问日志同样只落一条 requirement-summary。
  • 只影响「用房 · 汇总」区块的首屏。「用车 · 汇总」拿得到数据(它是后发的那条),其余三个区块调的是别的接口,不受影响。点「刷新」或整团确认后的 reload() 都能恢复。
  • 09-22 白天的 order-v3 应用日志已被滚动删除(只保留 3 份),那个窗口 13 次请求的「后端成功」由 nginx 200 + 响应体长度(与 09-23 已被应用日志直接证实成功的载荷等长)支撑,不是应用层直接证据;09-23 的两次有应用日志直接证实。
  • 前端侧的最初取证来自测试服实际部署的构建产物(/var/www/hl-admin/assets/,构建于 09-23 09:59),本条正文里的文件行号是事后对 origin/v2.1 = 61a549bd 源码复核后给出的,两侧一致。
  • 顺带一个非本单的观察:进入团期详情页时,页面在路由参数就绪前会发三条空 id 请求——group-batch//requirement-summary、group-batch//vehicle-requirement、group-batch//requirement/hotel-households(各返 132 B 业务错误)。来源不是 RoomSummarySection(它有 !groupBatchId 守卫)。对功能无影响,列在这里供一并排查。