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; attempted: boolean; result: CommandResult | null; rejected: boolean; operation?: 'ROUTE'; itemId?: string } const storageKey = (scope: string) => `dispatch-command:${scope}` const snapshot = (value: T): T => JSON.parse(JSON.stringify(value)) as T const sessions = new Map>; busy: ReturnType> }>() 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(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 = {}) { if (pending.value) throw new Error('上次排车操作结果尚未核对,请先恢复原请求') pending.value = { dispatchNo, action, body: snapshot(body) as Record, attempted: false, result: null, rejected: false, ...target } persist() } async function execute(send: (command: PendingDispatchCommand) => Promise, read: (dispatchNo: string) => Promise, find: (dispatchNo: string, key: string) => Promise) { 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 } }