Files
admin-web/src/modules/overseas-warehouse/dispatchCommandRecovery.ts
T
2026-10-19 17:56:31 +08:00

83 lines
5.2 KiB
TypeScript

import { ref } from 'vue'
import { ApiResponseError } from '@/shared/api/http'
import type { CommandResult, DispatchAction } from './dispatchContracts'
import { requireVersion } from './dispatchContracts'
export interface PendingDispatchCommand { dispatchNo: string; action: DispatchAction; body: Record<string, unknown>; attempted: boolean; result: CommandResult | null; rejected: boolean; operation?: 'ROUTE'; itemId?: string }
const storageKey = (scope: string) => `dispatch-command:${scope}`
const snapshot = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
const sessions = new Map<string, { pending: ReturnType<typeof ref<PendingDispatchCommand | null>>; busy: ReturnType<typeof ref<boolean>> }>()
export function isDefiniteDispatchRejection(error: unknown) {
return error instanceof ApiResponseError && /^(?:A0|HTTP_)?(?:400|401|403|404|409|410|422)$/.test(String(error.code))
}
export function validateCommandResult(result: CommandResult, pending: PendingDispatchCommand) {
if (!result || result.dispatchNo !== pending.dispatchNo && pending.action !== 'CREATE'
|| result.action !== pending.action || result.idempotencyKey !== pending.body.idempotencyKey
|| !result.dispatchNo || !result.result) throw new Error('操作结果与原排车请求不一致,请保留原请求核对')
requireVersion(result.recordVersion)
if (pending.action === 'DEPART' && result.stage !== 'DELIVERING') throw new Error('发车结果未确认派送中,请核对原请求')
if (pending.action === 'LOADING_COMPLETE' && result.stage !== 'AFTER_LOADING') throw new Error('装车结果未确认装车后,请核对原请求')
}
export function validateCurrentDispatch(value: unknown, result: CommandResult) {
const current = value as { dispatchNo?: string; recordVersion?: unknown } | null
if (current?.dispatchNo !== result.dispatchNo) throw new Error('恢复详情与原排车单不一致')
const latest = String(requireVersion(current.recordVersion)).replace(/^0+(?=\d)/, '')
const confirmed = String(requireVersion(result.recordVersion)).replace(/^0+(?=\d)/, '')
if (latest.length < confirmed.length || latest.length === confirmed.length && latest < confirmed) throw new Error('详情版本早于已确认操作,请保留原请求重读')
}
/** A response proves the original command; a subsequent GET supplies current UI facts. */
export function useDispatchCommandRecovery(scope: () => string) {
const owner = scope()
const saved = owner ? sessionStorage.getItem(storageKey(owner)) : null
const existing = sessions.get(owner)
const pending = existing?.pending || ref<PendingDispatchCommand | null>(null)
const busy = existing?.busy || ref(false)
if (!existing && saved) {
try { pending.value = JSON.parse(saved) as PendingDispatchCommand }
catch { throw new Error('本次排车请求记录损坏,请保留现场核对') }
}
if (!existing) sessions.set(owner, { pending, busy })
function persist() {
if (!owner || owner !== scope()) throw new Error('账号已变化,请重新进入排车列表')
if (pending.value) sessionStorage.setItem(storageKey(owner), JSON.stringify(pending.value))
else sessionStorage.removeItem(storageKey(owner))
}
function begin(dispatchNo: string, action: DispatchAction, body: object, target: Pick<PendingDispatchCommand, 'operation' | 'itemId'> = {}) {
if (pending.value) throw new Error('上次排车操作结果尚未核对,请先恢复原请求')
pending.value = { dispatchNo, action, body: snapshot(body) as Record<string, unknown>, attempted: false, result: null, rejected: false, ...target }
persist()
}
async function execute(send: (command: PendingDispatchCommand) => Promise<CommandResult>, read: (dispatchNo: string) => Promise<unknown>, find: (dispatchNo: string, key: string) => Promise<CommandResult>) {
if (busy.value || !pending.value) return
const command = pending.value
const previouslyAttempted = command.attempted
busy.value = true
try {
persist()
if (!command.result && previouslyAttempted && command.dispatchNo) {
try { const found = await find(command.dispatchNo, String(command.body.idempotencyKey)); validateCommandResult(found, command); command.result = found }
catch (error) { if (!(error instanceof ApiResponseError) || !['404', 'A0404', 'HTTP_404'].includes(String(error.code))) throw error }
}
if (!command.result) {
command.attempted = true; persist()
try { const sent = await send(snapshot(command)); validateCommandResult(sent, command); command.result = sent }
catch (error) {
// A later rejection cannot erase an earlier unknown write result.
if (!previouslyAttempted && isDefiniteDispatchRejection(error)) command.rejected = true
persist(); throw error
}
}
validateCommandResult(command.result, command); persist()
validateCurrentDispatch(await read(command.result.dispatchNo), command.result)
pending.value = null; persist()
return command.result
} finally { busy.value = false }
}
function discardRejected() {
if (busy.value || !pending.value?.rejected) return
pending.value = null; persist()
}
return { pending, busy, begin, execute, discardRejected }
}