Files
admin-web/src/modules/overseas-master-data/components/OwnedBusinessCommandRecovery.ts
T

90 lines
7.3 KiB
TypeScript
Raw Normal View History

2026-10-20 15:48:27 +08:00
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 }
}