90 lines
7.6 KiB
TypeScript
90 lines
7.6 KiB
TypeScript
import { ApiResponseError } from '@/shared/api/http'
|
|||
|
|
import * as api from '../api/outsourcedBusiness'
|
||
|
|
import { outsourcedBusinessPhotoRoles, outsourcedBusinessVersion, type OutsourcedBusinessAction, type OutsourcedBusinessBind, type OutsourcedBusinessCommand, type OutsourcedBusinessDetail, type OutsourcedBusinessPhoto, type OutsourcedBusinessPhotoSlot, type OutsourcedBusinessSave } from '../outsourcedBusinessTypes'
|
||
|
|
|
||
|
|
export interface OutsourcedBusinessPending {
|
||
|
|
actor: string; dispatchNo: string; action: OutsourcedBusinessAction; key: string; slot?: OutsourcedBusinessPhotoSlot
|
||
|
|
body: OutsourcedBusinessSave | OutsourcedBusinessBind | null; attempted: boolean; command: OutsourcedBusinessCommand | null; confirmed: { recordVersion: string | null; result: OutsourcedBusinessDetail | OutsourcedBusinessPhoto } | null; rejected: boolean
|
||
|
|
}
|
||
|
|
export const outsourcedBusinessPendingKey = (actor: string, no: string, area: string) => `outsourced-business:${encodeURIComponent(actor)}:${encodeURIComponent(no)}:${area}`
|
||
|
|
export function restoreOutsourcedBusinessPending(actor: string, no: string, area: string): OutsourcedBusinessPending | null {
|
||
|
|
const raw = sessionStorage.getItem(outsourcedBusinessPendingKey(actor, no, area))
|
||
|
|
if (!raw) return null
|
||
|
|
const pending = JSON.parse(raw) as OutsourcedBusinessPending
|
||
|
|
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' && !outsourcedBusinessPhotoRoles.some(role => role.slot === pending.slot) || pending.action !== 'UPLOAD_PHOTO' && (!pending.body || pending.body.idempotencyKey !== pending.key)) throw new Error('原运营请求内容不完整,请保留现场核对')
|
||
|
|
return pending
|
||
|
|
}
|
||
|
|
export function persistOutsourcedBusinessPending(actor: string, no: string, area: string, pending: OutsourcedBusinessPending | null) {
|
||
|
|
if (!actor || !no || pending && (pending.actor !== actor || pending.dispatchNo !== no)) throw new Error('运营请求身份已变化')
|
||
|
|
const key = outsourcedBusinessPendingKey(actor, no, area)
|
||
|
|
if (pending) sessionStorage.setItem(key, JSON.stringify(pending)); else sessionStorage.removeItem(key)
|
||
|
|
}
|
||
|
|
export function newOutsourcedBusinessPending(actor: string, no: string, action: OutsourcedBusinessAction, body: OutsourcedBusinessPending['body'], slot?: OutsourcedBusinessPhotoSlot): OutsourcedBusinessPending {
|
||
|
|
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 acceptOutsourcedBusinessResult(pending: OutsourcedBusinessPending, result: OutsourcedBusinessDetail | OutsourcedBusinessPhoto) {
|
||
|
|
if (pending.action === 'UPLOAD_PHOTO') {
|
||
|
|
if (!('fileId' in result) || result.slot !== pending.slot || result.category !== 'LOADING' || 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: outsourcedBusinessVersion(result.row.recordVersion), result }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
export function acceptOutsourcedBusinessCommand(pending: OutsourcedBusinessPending, command: OutsourcedBusinessCommand) {
|
||
|
|
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('原运营命令的成功结果不完整,请继续核对')
|
||
|
|
acceptOutsourcedBusinessResult(pending, command.result)
|
||
|
|
if (pending.action !== 'UPLOAD_PHOTO' && (command.recordVersion == null || command.recordVersion !== pending.confirmed!.recordVersion)) throw new Error('原运营命令版本与结果不一致')
|
||
|
|
}
|
||
|
|
export async function recoverOutsourcedBusinessCommand(pending: OutsourcedBusinessPending, persist: () => void, ensureScope: () => void, client = api): Promise<{ detail?: OutsourcedBusinessDetail; photo?: OutsourcedBusinessPhoto }> {
|
||
|
|
ensureScope()
|
||
|
|
if (pending.rejected) throw new Error('原请求已明确拒绝,请先按明确拒绝结果重新核对')
|
||
|
|
if (pending.command?.state === 'SUCCEEDED') acceptOutsourcedBusinessCommand(pending, pending.command)
|
||
|
|
const previous = pending.attempted
|
||
|
|
if (!pending.confirmed) {
|
||
|
|
let original: OutsourcedBusinessCommand | null = null
|
||
|
|
if (previous) {
|
||
|
|
try { original = await client.getOutsourcedBusinessCommand(pending.dispatchNo, pending.key) }
|
||
|
|
catch (cause) { if (!(cause instanceof ApiResponseError) || !['404','A0404','HTTP_404'].includes(cause.code)) throw cause }
|
||
|
|
ensureScope()
|
||
|
|
}
|
||
|
|
if (original) { try { acceptOutsourcedBusinessCommand(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.saveOutsourcedBusinessOperations(pending.dispatchNo, pending.body as OutsourcedBusinessSave)
|
||
|
|
: await client.bindOutsourcedBusinessPhoto(pending.dispatchNo, pending.slot!, pending.body as OutsourcedBusinessBind)
|
||
|
|
ensureScope(); acceptOutsourcedBusinessResult(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 OutsourcedBusinessPhoto | null
|
||
|
|
if (!photo || photo.slot !== pending.slot || photo.category !== 'LOADING' || 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.getOutsourcedBusiness(pending.dispatchNo); ensureScope()
|
||
|
|
if (current.row.dispatchNo !== pending.dispatchNo || confirmed.recordVersion == null || BigInt(outsourcedBusinessVersion(current.row.recordVersion)) < BigInt(outsourcedBusinessVersion(confirmed.recordVersion))) throw new Error('当前运营版本早于原操作结果,请继续核对')
|
||
|
|
if (pending.action === 'BIND_PHOTO') {
|
||
|
|
const matches = (photo: OutsourcedBusinessPhoto) => photo.slot === pending.slot && photo.fileId === (pending.body as OutsourcedBusinessBind).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 }
|
||
|
|
}
|