feat:抢单大厅数据排列优化

This commit is contained in:
2026-10-25 14:41:13 +08:00
parent 9c620d39cb
commit d787d3c364
6 changed files with 124 additions and 5 deletions
@@ -21,6 +21,13 @@ type UseRealtimeEventsOptions = {
token: string
onEvent: (event: RealtimeEvent) => void
onConnected?: () => void
/**
* 服务端告知「补发窗口溢出,历史事件已不连续」。
*
* 此时不能再依赖逐条快照补丁(会把列表写回旧值),调用方应整体失效对应查询,
* 让列表接口重新给出权威数据。
*/
onResync?: (info: { headEventId: number; reason: string }) => void
/** 鉴权失效(401/403)时触发:连接停止重连,调用方应清理会话并跳转登录页。 */
onAuthExpired?: () => void
}
@@ -33,13 +40,16 @@ export function useRealtimeEvents({
token,
onEvent,
onConnected,
onResync,
onAuthExpired,
}: UseRealtimeEventsOptions) {
const onEventRef = useRef(onEvent)
const onConnectedRef = useRef(onConnected)
const onResyncRef = useRef(onResync)
const onAuthExpiredRef = useRef(onAuthExpired)
onEventRef.current = onEvent
onConnectedRef.current = onConnected
onResyncRef.current = onResync
onAuthExpiredRef.current = onAuthExpired
useEffect(() => {
@@ -79,6 +89,15 @@ export function useRealtimeEvents({
}
onConnectedRef.current?.()
await readSseStream(response.body, (eventName, data) => {
// 补发窗口溢出:历史事件不连续,不能逐条复位快照,交给调用方整体重拉。
if (eventName === 'resync') {
const notice = normalizeResyncNotice(data)
if (notice) {
lastEventId = Math.max(lastEventId, notice.headEventId)
onResyncRef.current?.(notice)
}
return
}
if (eventName !== 'realtime') return
const event = normalizeRealtimeEvent(data)
if (event) {
@@ -197,3 +216,16 @@ function normalizeRealtimeEvent(value: string): RealtimeEvent | null {
return null
}
}
/** 服务端 resync 通知:补发窗口溢出,客户端应把游标推进到 headEventId 并整体重拉。 */
function normalizeResyncNotice(value: string): { headEventId: number; reason: string } | null {
try {
const parsed = JSON.parse(value) as { headEventId?: unknown; reason?: unknown }
return {
headEventId: Math.max(0, Number(parsed.headEventId) || 0),
reason: String(parsed.reason || ''),
}
} catch {
return null
}
}
+10
View File
@@ -119,6 +119,16 @@ export default function AdminLayout() {
url: '/api/v1/admin/realtime',
token: role === 'finance' ? '' : getAdminToken(),
onEvent: (event) => applyAdminRealtimeEvent(event, queryClient),
// 补发窗口溢出(断线太久):历史事件已不连续,整体失效让接口重新给权威数据。
onResync: () => {
void queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] })
void queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-after-sales-cases'] })
void queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-finance-requests'] })
void queryClient.invalidateQueries({ queryKey: ['admin-notifications'] })
void queryClient.invalidateQueries({ queryKey: ['admin-tasks'] })
void queryClient.invalidateQueries({ queryKey: ['admin-orders'] })
void queryClient.invalidateQueries({ queryKey: ['admin-dashboard-summary'] })
},
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
onAuthExpired: () => {
clearAdminSession()
@@ -138,6 +138,16 @@ export default function WorkerLayout() {
url: '/api/v1/worker/realtime',
token: getWorkerToken(),
onEvent: (event) => applyWorkerRealtimeEvent(event, queryClient),
// 补发窗口溢出(断线太久):历史事件已不连续,逐条套旧快照会把列表写回旧值,
// 这里整体失效,让列表接口重新给权威数据。
onResync: () => {
void queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] })
void queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
void queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] })
void queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] })
void queryClient.invalidateQueries({ queryKey: ['worker-finance-requests'] })
void queryClient.invalidateQueries({ queryKey: ['worker-profile'] })
},
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
onAuthExpired: () => {
expireWorkerSession()