修复接单端过期会话重复请求
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { resolveRouteErrorLogLevel } from './http.js'
|
||||
|
||||
test('expected worker session expiry is debug-level', () => {
|
||||
assert.equal(
|
||||
resolveRouteErrorLogLevel({ statusCode: 401, errorCode: 'worker_auth_expired' }),
|
||||
'debug',
|
||||
)
|
||||
assert.equal(
|
||||
resolveRouteErrorLogLevel({ statusCode: 401, errorCode: 'worker_auth_invalid' }),
|
||||
'warn',
|
||||
)
|
||||
assert.equal(resolveRouteErrorLogLevel({ statusCode: 500, errorCode: 'internal_error' }), 'error')
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { logError, logWarn } from './logger.js'
|
||||
import { logDebug, logError, logWarn } from './logger.js'
|
||||
|
||||
const RESPONSE_TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
@@ -52,7 +52,8 @@ export function sendRouteError(
|
||||
scope = '[route]',
|
||||
): void {
|
||||
const classification = classifyRouteError(error)
|
||||
const logger = classification.statusCode >= 500 ? logError : logWarn
|
||||
const logLevel = resolveRouteErrorLogLevel(classification)
|
||||
const logger = logLevel === 'error' ? logError : logLevel === 'debug' ? logDebug : logWarn
|
||||
logger(scope, fallbackMessage, {
|
||||
statusCode: classification.statusCode,
|
||||
errorCode: classification.errorCode,
|
||||
@@ -62,6 +63,15 @@ export function sendRouteError(
|
||||
res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage))
|
||||
}
|
||||
|
||||
export function resolveRouteErrorLogLevel(classification: {
|
||||
statusCode: number
|
||||
errorCode: string
|
||||
}): 'debug' | 'warn' | 'error' {
|
||||
if (classification.statusCode >= 500) return 'error'
|
||||
if (classification.errorCode === 'worker_auth_expired') return 'debug'
|
||||
return 'warn'
|
||||
}
|
||||
|
||||
export function buildNotFoundPayload(req: Request) {
|
||||
return {
|
||||
code: 1,
|
||||
|
||||
@@ -8,12 +8,21 @@ import {
|
||||
import { App, Button, Layout, Menu, Typography } from 'antd'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { useEffect } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||
|
||||
import { logoutWorker } from '@/services/worker'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
import { clearWorkerSession, getWorkerStatus, getWorkerUsername } from '@/utils/worker-auth'
|
||||
import { getWorkerToken } from '@/utils/worker-auth'
|
||||
import {
|
||||
clearWorkerSession,
|
||||
expireWorkerSession,
|
||||
getWorkerSessionExpiryDelay,
|
||||
getWorkerStatus,
|
||||
getWorkerToken,
|
||||
getWorkerTokenExpiresAt,
|
||||
getWorkerUsername,
|
||||
isWorkerSessionStorageKey,
|
||||
} from '@/utils/worker-auth'
|
||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||
import WorkerAnnouncementBanner from '@/components/worker/WorkerAnnouncementBanner'
|
||||
import WorkerNewOrderSpeech from '@/components/worker/WorkerNewOrderSpeech'
|
||||
@@ -41,14 +50,45 @@ export default function WorkerLayout() {
|
||||
const status = getWorkerStatus()
|
||||
const currentKey = resolveSelectedKey(location.pathname)
|
||||
|
||||
useEffect(() => {
|
||||
const navigateToLogin = () => {
|
||||
if (window.location.pathname !== '/') {
|
||||
void navigate('/', { replace: true })
|
||||
}
|
||||
}
|
||||
const handleSessionExpired = () => navigateToLogin()
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (isWorkerSessionStorageKey(event.key) && event.newValue === null) {
|
||||
expireWorkerSession()
|
||||
navigateToLogin()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('worker-session-expired', handleSessionExpired)
|
||||
window.addEventListener('storage', handleStorage)
|
||||
return () => {
|
||||
window.removeEventListener('worker-session-expired', handleSessionExpired)
|
||||
window.removeEventListener('storage', handleStorage)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
const delay = getWorkerSessionExpiryDelay(getWorkerTokenExpiresAt())
|
||||
if (delay === null) return
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
expireWorkerSession()
|
||||
}, delay)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
useRealtimeEvents({
|
||||
url: '/api/v1/worker/realtime',
|
||||
token: getWorkerToken(),
|
||||
onEvent: (event) => applyWorkerRealtimeEvent(event, queryClient),
|
||||
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
|
||||
onAuthExpired: () => {
|
||||
clearWorkerSession()
|
||||
void navigate('/', { replace: true })
|
||||
expireWorkerSession()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import { clearAdminSession, getAdminToken } from '@/utils/admin-auth'
|
||||
import { isWorkerSite } from '@/utils/site'
|
||||
import { clearWorkerSession, getWorkerToken } from '@/utils/worker-auth'
|
||||
import { expireWorkerSession, getWorkerToken } from '@/utils/worker-auth'
|
||||
|
||||
export interface ApiEnvelope<T> {
|
||||
code: number
|
||||
@@ -73,7 +73,7 @@ http.interceptors.response.use(
|
||||
const errorCode = String(error?.response?.data?.errorCode || '').trim()
|
||||
|
||||
if (status === 401 && shouldClearWorkerSession(errorCode)) {
|
||||
clearWorkerSession()
|
||||
expireWorkerSession()
|
||||
|
||||
if (isWorkerSite() && window.location.pathname !== '/') {
|
||||
window.location.replace('/')
|
||||
|
||||
@@ -5,6 +5,14 @@ const WORKER_USERNAME_KEY = 'order-site-worker-username'
|
||||
const WORKER_STATUS_KEY = 'order-site-worker-status'
|
||||
const WORKER_DEVICE_ID_KEY = 'order-site-worker-device-id'
|
||||
|
||||
const WORKER_SESSION_STORAGE_KEYS = new Set([
|
||||
WORKER_TOKEN_KEY,
|
||||
WORKER_EXPIRES_AT_KEY,
|
||||
WORKER_ID_KEY,
|
||||
WORKER_USERNAME_KEY,
|
||||
WORKER_STATUS_KEY,
|
||||
])
|
||||
|
||||
export function getWorkerToken() {
|
||||
return localStorage.getItem(WORKER_TOKEN_KEY) || ''
|
||||
}
|
||||
@@ -65,6 +73,27 @@ export function clearWorkerSession() {
|
||||
localStorage.removeItem(WORKER_STATUS_KEY)
|
||||
}
|
||||
|
||||
/** 清理失效会话并通知当前标签页,避免并发请求各自重复跳转。 */
|
||||
export function expireWorkerSession() {
|
||||
const hadSession = Boolean(getWorkerToken() || getWorkerTokenExpiresAt())
|
||||
clearWorkerSession()
|
||||
if (hadSession && typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new Event('worker-session-expired'))
|
||||
}
|
||||
return hadSession
|
||||
}
|
||||
|
||||
export function isWorkerSessionStorageKey(key: string | null) {
|
||||
return Boolean(key && WORKER_SESSION_STORAGE_KEYS.has(key))
|
||||
}
|
||||
|
||||
export function getWorkerSessionExpiryDelay(expiresAt: string, now = Date.now()) {
|
||||
if (!expiresAt) return null
|
||||
const expiresAtMs = Date.parse(expiresAt)
|
||||
if (!Number.isFinite(expiresAtMs)) return null
|
||||
return Math.max(0, expiresAtMs - now)
|
||||
}
|
||||
|
||||
export function hasWorkerSession() {
|
||||
const token = getWorkerToken()
|
||||
const expiresAt = getWorkerTokenExpiresAt()
|
||||
@@ -77,7 +106,6 @@ export function hasWorkerSession() {
|
||||
}
|
||||
|
||||
function isExpired(expiresAt: string) {
|
||||
if (!expiresAt) return false
|
||||
const expiresAtMs = Date.parse(expiresAt)
|
||||
return Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()
|
||||
const delay = getWorkerSessionExpiryDelay(expiresAt)
|
||||
return delay !== null && delay <= 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { getWorkerSessionExpiryDelay } from '../src/utils/worker-auth.ts'
|
||||
|
||||
test('worker session expiry delay handles valid, expired, and invalid timestamps', () => {
|
||||
const now = Date.parse('2026-08-30T09:00:00.000Z')
|
||||
assert.equal(getWorkerSessionExpiryDelay('2026-08-30T09:00:05.000Z', now), 5_000)
|
||||
assert.equal(getWorkerSessionExpiryDelay('2026-08-30T08:59:59.000Z', now), 0)
|
||||
assert.equal(getWorkerSessionExpiryDelay('not-a-date', now), null)
|
||||
assert.equal(getWorkerSessionExpiryDelay('', now), null)
|
||||
})
|
||||
Reference in New Issue
Block a user