import { ApiResponseError } from '@/shared/api/http' import * as api from '../api/ownedBusiness' import { ownedBusinessPhotoRoles, ownedBusinessVersion, type OwnedBusinessAction, type OwnedBusinessBind, type OwnedBusinessCommand, type OwnedBusinessDetail, type OwnedBusinessPhoto, type OwnedBusinessPhotoSlot, type OwnedBusinessSave } from '../ownedBusinessTypes' export interface OwnedBusinessPending { actor: string; dispatchNo: string; action: OwnedBusinessAction; key: string; slot?: OwnedBusinessPhotoSlot body: OwnedBusinessSave | OwnedBusinessBind | null; attempted: boolean; command: OwnedBusinessCommand | null; confirmed: { recordVersion: string | null; result: OwnedBusinessDetail | OwnedBusinessPhoto } | null; rejected: boolean } export const ownedBusinessPendingKey = (actor: string, no: string, area: string) => `owned-business:${encodeURIComponent(actor)}:${encodeURIComponent(no)}:${area}` export function restoreOwnedBusinessPending(actor: string, no: string, area: string): OwnedBusinessPending | null { const raw = sessionStorage.getItem(ownedBusinessPendingKey(actor, no, area)) if (!raw) return null const pending = JSON.parse(raw) as OwnedBusinessPending if (pending.actor !== actor || pending.dispatchNo !== no || !pending.key || !['SAVE_OPERATIONS','UPLOAD_PHOTO','BIND_PHOTO'].includes(pending.action)) throw new Error('原运营请求记录损坏,请保留现场核对') if (pending.action !== 'SAVE_OPERATIONS' && !ownedBusinessPhotoRoles.some(role => role.slot === pending.slot) || pending.action !== 'UPLOAD_PHOTO' && (!pending.body || pending.body.idempotencyKey !== pending.key)) throw new Error('原运营请求内容不完整,请保留现场核对') return pending } export function persistOwnedBusinessPending(actor: string, no: string, area: string, pending: OwnedBusinessPending | null) { if (!actor || !no || pending && (pending.actor !== actor || pending.dispatchNo !== no)) throw new Error('运营请求身份已变化') const key = ownedBusinessPendingKey(actor, no, area) if (pending) sessionStorage.setItem(key, JSON.stringify(pending)); else sessionStorage.removeItem(key) } export function newOwnedBusinessPending(actor: string, no: string, action: OwnedBusinessAction, body: OwnedBusinessPending['body'], slot?: OwnedBusinessPhotoSlot): OwnedBusinessPending { if (!actor || !no) throw new Error('当前运营作业身份缺失') const key = body?.idempotencyKey || crypto.randomUUID() return { actor, dispatchNo: no, action, key, slot, body: body ? structuredClone(body) : null, attempted: false, command: null, confirmed: null, rejected: false } } export function acceptOwnedBusinessResult(pending: OwnedBusinessPending, result: OwnedBusinessDetail | OwnedBusinessPhoto) { if (pending.action === 'UPLOAD_PHOTO') { if (!('fileId' in result) || result.slot !== pending.slot || typeof result.fileId !== 'string' || !/^[1-9]\d*$/.test(result.fileId) || !['image/jpeg','image/png'].includes(result.contentType)) throw new Error('原上传尚未返回本分类的真实文件') pending.confirmed = { recordVersion: null, result } } else { if (!('row' in result) || result.row.dispatchNo !== pending.dispatchNo) throw new Error('运营保存结果与原排车单不一致') pending.confirmed = { recordVersion: ownedBusinessVersion(result.row.recordVersion), result } } } export function acceptOwnedBusinessCommand(pending: OwnedBusinessPending, command: OwnedBusinessCommand) { if (command.dispatchNo !== pending.dispatchNo || command.action !== pending.action || command.idempotencyKey !== pending.key) throw new Error('返回结果与原运营请求不一致,请保留原请求核对') pending.command = command if (command.state === 'REJECTED') pending.rejected = true if (command.state !== 'SUCCEEDED') throw new Error(command.state === 'REJECTED' ? '原请求已明确拒绝,请核对原因后重新登记' : '原请求结果尚未确定,请保留原请求继续核对') if (!command.result) throw new Error('原运营命令的成功结果不完整,请继续核对') acceptOwnedBusinessResult(pending, command.result) if (pending.action !== 'UPLOAD_PHOTO' && (command.recordVersion == null || command.recordVersion !== pending.confirmed!.recordVersion)) throw new Error('原运营命令版本与结果不一致') } export async function recoverOwnedBusinessCommand(pending: OwnedBusinessPending, persist: () => void, ensureScope: () => void, client = api): Promise<{ detail?: OwnedBusinessDetail; photo?: OwnedBusinessPhoto }> { ensureScope() if (pending.rejected) throw new Error('原请求已明确拒绝,请先按明确拒绝结果重新核对') if (pending.command?.state === 'SUCCEEDED') acceptOwnedBusinessCommand(pending, pending.command) const previous = pending.attempted if (!pending.confirmed) { let original: OwnedBusinessCommand | null = null if (previous) { try { original = await client.getOwnedBusinessCommand(pending.dispatchNo, pending.key) } catch (cause) { if (!(cause instanceof ApiResponseError) || !['404','A0404','HTTP_404'].includes(cause.code)) throw cause } ensureScope() } if (original) { try { acceptOwnedBusinessCommand(pending, original) } finally { persist() } } else { if (pending.action === 'UPLOAD_PHOTO') throw new Error('图片上传结果未知,禁止重复上传;请继续核对原上传请求') pending.attempted = true; persist() try { const result = pending.action === 'SAVE_OPERATIONS' ? await client.saveOwnedBusinessOperations(pending.dispatchNo, pending.body as OwnedBusinessSave) : await client.bindOwnedBusinessPhoto(pending.dispatchNo, pending.slot!, pending.body as OwnedBusinessBind) ensureScope(); acceptOwnedBusinessResult(pending, result); persist() } catch (cause) { ensureScope() if (!previous && cause instanceof ApiResponseError && ['A0400','A0403','A0404','A0409','400','403','404','409','422'].includes(cause.code)) { pending.rejected = true; persist() } throw cause } } } const confirmed = pending.confirmed! if (pending.action === 'UPLOAD_PHOTO') { const photo = confirmed.result as OwnedBusinessPhoto | null if (!photo || photo.slot !== pending.slot || typeof photo.fileId !== 'string' || !/^[1-9]\d*$/.test(photo.fileId) || !['image/jpeg','image/png'].includes(photo.contentType)) throw new Error('原上传尚未返回本分类的真实文件') return { photo } } const current = await client.getOwnedBusiness(pending.dispatchNo); ensureScope() if (current.row.dispatchNo !== pending.dispatchNo || confirmed.recordVersion == null || BigInt(ownedBusinessVersion(current.row.recordVersion)) < BigInt(ownedBusinessVersion(confirmed.recordVersion))) throw new Error('当前运营版本早于原操作结果,请继续核对') if (pending.action === 'BIND_PHOTO') { const matches = (photo: OwnedBusinessPhoto) => photo.slot === pending.slot && photo.fileId === (pending.body as OwnedBusinessBind).fileId && Boolean(photo.savedAt) const original = confirmed.result const originalPhotos = original && 'row' in original ? original.photos : original && 'fileId' in original ? [original] : [] if (!current.photos.some(matches) && !originalPhotos.some(matches)) throw new Error('图片绑定结果尚未确认,请保留原文件和请求') } return { detail: current } }