修复接单端过期会话重复请求

This commit is contained in:
yml2213
2026-08-30 17:32:34 +08:00
parent 535cefd4de
commit ffb71f2276
6 changed files with 117 additions and 11 deletions
+16
View File
@@ -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')
})
+12 -2
View File
@@ -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,
+44 -4
View File
@@ -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()
},
})
+2 -2
View File
@@ -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('/')
+31 -3
View File
@@ -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
}
+12
View File
@@ -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)
})