fix(gateway,frontend): 修复鉴权绕过与前端支付/会话缺陷

后端:
- Gemini /v1beta 鉴权中间件补齐主中间件的授权校验: API Key 的 IP 白/黑名单、
  专属分组授权、运行时过期/配额二次检查, 修复经 Gemini 端点绕过 IP ACL、
  越权访问专属分组、以及状态未刷新时的配额/有效期绕过窗口。
- 粘性会话等待计划分支改走 newSelectionResult 以 hydrate 账号凭证, 修复调度
  快照中账号凭证被剥离导致等待路径转发鉴权失败。
- SSE 流式转发客户端断开时不再 break 跳过当前事件 usage 合并, 修复少计费。
- Forward 对 nil gin.Context 的防御补齐; 上游错误体读取失败时记录日志避免静默。

前端:
- logout 将本地会话清理移入 finally, 服务端吊销失败也保证本地登出。
- Stripe 弹窗轮询改用正确的 auth_token 键并加防重入; 收到 INIT 后清除兜底
  超时定时器, onUnmounted 清理 message 监听器。
- token 刷新请求补充 30s 超时, 避免挂起导致请求队列与 loading 永久卡死。
- 路由守卫在公共设置未加载时先 await fetchPublicSettings, 避免 payment/
  risk_control 被误判为未启用而错误拦截。
- 支付状态轮询回调补充防重入与终态守卫。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
superman2003
2026-07-09 09:06:56 +08:00
committed by Clawd
co-authored by Cursor
parent 6f43986c37
commit 29a5fcd25e
10 changed files with 185 additions and 54 deletions
@@ -275,22 +275,33 @@ async function tryRecoverPendingOrder(order: PaymentOrder): Promise<PaymentOrder
}
}
let pollInFlight = false
async function pollStatus() {
if (!props.orderId || outcome.value) return
let order = await paymentStore.pollOrderStatus(props.orderId)
if (!order) return
order = await tryRecoverPendingOrder(order)
if (isSuccessStatus(order.status)) {
cleanup()
paidOrder.value = order
setOutcome('success')
emit('success')
} else if (order.status === 'CANCELLED') {
cleanup()
setOutcome('cancelled')
} else if (order.status === 'EXPIRED' || order.status === 'FAILED') {
cleanup()
setOutcome('expired')
// 防重入:接口(含 verifyOrder 二次确认)响应慢于 3 秒轮询间隔时避免并发重叠请求。
if (pollInFlight) return
pollInFlight = true
try {
let order = await paymentStore.pollOrderStatus(props.orderId)
if (!order) return
// 已进入终态则不再处理迟到的响应。
if (outcome.value) return
order = await tryRecoverPendingOrder(order)
if (outcome.value) return
if (isSuccessStatus(order.status)) {
cleanup()
paidOrder.value = order
setOutcome('success')
emit('success')
} else if (order.status === 'CANCELLED') {
cleanup()
setOutcome('cancelled')
} else if (order.status === 'EXPIRED' || order.status === 'FAILED') {
cleanup()
setOutcome('expired')
}
} finally {
pollInFlight = false
}
}