This commit is contained in:
2026-08-08 10:11:04 +08:00
parent f3c2bafec8
commit fc14856ea3
64 changed files with 50932 additions and 30 deletions
@@ -0,0 +1,452 @@
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
import process from 'node:process'
const MODULE = 'inbound-weight-ocr'
const SCENARIO = 'weight-image-recognition-and-confirmation'
const DEFAULT_IMAGE = 'C:/Users/BY-OY/Desktop/上传图片包/测量图片/测量图片/orig_1cf27695-b2c3-44b3-98e3-c0be8b80f79a.JPG'
const EDITABLE_STATUSES = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
const OCR_STATUSES = new Set(['RECOGNIZED', 'FAILED'])
const NEW_CONTRACT_OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
const SCENARIO_OPTIONS = { scenario: SCENARIO }
function id(value) {
return value == null ? '' : String(value)
}
function apiOk(response) {
return response?.http === 200 && String(response?.body?.code) === '200'
}
function closeNumber(actual, expected, tolerance = 0.0005) {
return Number.isFinite(Number(actual)) && Math.abs(Number(actual) - Number(expected)) < tolerance
}
function expectedConflict(response) {
return response?.http === 200 && ['A0409', '409'].includes(String(response?.body?.code))
}
function expectedRejection(response) {
return response?.http === 200 && response?.body && String(response.body.code) !== '200'
}
function markerVisible(detail, marker) {
const values = [
detail?.publicRemark,
detail?.summary?.inboundCode,
detail?.summary?.orderNo,
detail?.summary?.customerName,
detail?.summary?.companyName,
...(Array.isArray(detail?.internalNotes) ? detail.internalNotes.map((note) => note?.content) : []),
]
return values.some((value) => typeof value === 'string' && value.includes(marker))
}
function candidateValues(context) {
const shared = context.context || {}
return [
shared.inboundWeightOcr,
shared.ocrInbound,
shared.testInbound,
shared.createdInbound,
shared.inboundId && shared.tenantId
? { inboundId: shared.inboundId, tenantId: shared.tenantId, marker: shared.inboundMarker }
: null,
].filter(Boolean)
}
async function resolveMarkedInbound(context) {
for (const candidate of candidateValues(context)) {
const tenantId = id(candidate.tenantId || candidate.customerId)
const inboundId = id(candidate.inboundId || candidate.id)
if (!tenantId || !inboundId) continue
const response = await context.request(
'GET', `/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}`,
)
const detail = apiOk(response) ? response.body.data : null
const marked = markerVisible(detail, context.marker)
const editable = EDITABLE_STATUSES.has(detail?.summary?.processStatus)
if (detail && marked && editable) return { tenantId, inboundId, detail, response }
}
const filters = [
`inboundCode=${encodeURIComponent(context.marker)}`,
`orderNo=${encodeURIComponent(context.marker)}`,
]
for (const filter of filters) {
const pageResponse = await context.request('GET', `/api/platform/inbounds?page=1&size=20&${filter}`)
if (!apiOk(pageResponse)) continue
for (const row of pageResponse.body.data?.records || []) {
const tenantId = id(row.tenantId)
const inboundId = id(row.id)
if (!tenantId || !inboundId || !EDITABLE_STATUSES.has(row.processStatus)) continue
const detailResponse = await context.request(
'GET', `/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}`,
)
const detail = apiOk(detailResponse) ? detailResponse.body.data : null
if (detail && markerVisible(detail, context.marker)) {
return { tenantId, inboundId, detail, response: detailResponse }
}
}
}
return null
}
async function multipart(context, path, file, token = context.token) {
const form = new FormData()
form.append('file', new Blob([file.bytes], { type: file.contentType }), file.name)
const headers = { Accept: 'application/json' }
if (token) headers.Authorization = `Bearer ${token}`
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method: 'POST', headers, body: form, signal: AbortSignal.timeout(120_000),
})
const content = await response.text()
let payload = null
try { payload = content ? JSON.parse(content) : null } catch { /* Keep bounded diagnostic text only. */ }
return {
method: 'POST', path, http: response.status, body: payload,
raw: payload === null ? content.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id'),
}
} catch (error) {
return {
method: 'POST', path, http: 0, body: null,
raw: error instanceof Error ? error.message : String(error),
requestId: null, traceId: null,
}
}
}
function jpegDimensions(bytes) {
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return null
const standalone = new Set([0x01, 0xd8, 0xd9, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7])
const startsOfFrame = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf])
let offset = 2
while (offset + 3 < bytes.length) {
while (offset < bytes.length && bytes[offset] !== 0xff) offset += 1
while (offset < bytes.length && bytes[offset] === 0xff) offset += 1
if (offset >= bytes.length) break
const marker = bytes[offset]
offset += 1
if (standalone.has(marker)) continue
if (offset + 1 >= bytes.length) break
const length = bytes[offset] * 256 + bytes[offset + 1]
if (length < 2 || offset + length > bytes.length) break
if (startsOfFrame.has(marker) && length >= 7) {
return {
height: bytes[offset + 3] * 256 + bytes[offset + 4],
width: bytes[offset + 5] * 256 + bytes[offset + 6],
}
}
offset += length
}
return null
}
function workspacePath(tenantId, inboundId) {
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/measurement?tenantId=${encodeURIComponent(tenantId)}`
}
function resultPath(inboundId, resultId) {
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/weight-results/${encodeURIComponent(resultId)}`
}
function confirmPath(tenantId, inboundId, resultId) {
return `${resultPath(inboundId, resultId)}/confirm?tenantId=${encodeURIComponent(tenantId)}`
}
function deletePath(tenantId, inboundId, resultId, version) {
return `${resultPath(inboundId, resultId)}?tenantId=${encodeURIComponent(tenantId)}&version=${version}`
}
async function createUploadedResult(context, target, image, overrides = {}) {
const imagePath = `/api/platform/v1/files/measurements/${encodeURIComponent(target.tenantId)}/${encodeURIComponent(target.inboundId)}`
const uploadResponse = await multipart(context, imagePath, image)
const upload = context.apiSuccess(MODULE, '上传称重图片并执行 OCR', uploadResponse,
`file=${image.name}, size=${image.bytes.length}`, SCENARIO_OPTIONS)
const fileId = id(upload?.fileId)
context.assert(MODULE, '上传响应保持文件 ID 字符串契约', uploadResponse,
Boolean(fileId) && typeof upload?.fileId === 'string',
`fileIdType=${typeof upload?.fileId}, status=${upload?.recognitionStatus ?? 'missing'}`, SCENARIO_OPTIONS)
context.assert(MODULE, 'OCR 上传响应字段完整且置信度受限', uploadResponse,
upload && OCR_STATUSES.has(upload.recognitionStatus)
&& Object.hasOwn(upload, 'weightValue') && Object.hasOwn(upload, 'weightUnit')
&& Object.hasOwn(upload, 'confidence')
&& (upload.confidence == null || (Number(upload.confidence) >= 0 && Number(upload.confidence) <= 1)),
`status=${upload?.recognitionStatus ?? 'missing'}, hasWeight=${upload?.weightValue != null}, confidence=${upload?.confidence ?? 'null'}`,
SCENARIO_OPTIONS)
if (!fileId) throw new Error('称重图片上传未返回 fileId,无法登记和清理测试资源')
let recognized = upload
if (overrides.retry) {
const retryPath = `${imagePath}/${encodeURIComponent(fileId)}/recognize`
const retryResponse = await context.request('POST', retryPath)
const retry = context.apiSuccess(MODULE, '重试当前称重图片 OCR', retryResponse,
`fileId=${fileId}`, SCENARIO_OPTIONS)
context.assert(MODULE, 'OCR 重试保持文件身份并返回识别契约', retryResponse,
id(retry?.fileId) === fileId && OCR_STATUSES.has(retry?.recognitionStatus)
&& Object.hasOwn(retry || {}, 'weightValue') && Object.hasOwn(retry || {}, 'confidence'),
`sameFile=${id(retry?.fileId) === fileId}, status=${retry?.recognitionStatus ?? 'missing'}`,
SCENARIO_OPTIONS)
if (retry) recognized = retry
}
const payload = {
fileId,
ocrProvider: `ADMIN_ACCEPTANCE_${context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-24)}`,
ocrRawText: overrides.manual ? null : (recognized.ocrRawText || null),
ocrWeightValue: overrides.manual ? null : recognized.weightValue,
ocrWeightUnit: overrides.manual ? null : recognized.weightUnit,
confidence: overrides.manual ? null : recognized.confidence,
}
const createPath = `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(target.inboundId)}/weight-results?tenantId=${encodeURIComponent(target.tenantId)}`
let createResponse = await context.request('POST', createPath, payload)
let result = context.apiSuccess(MODULE, overrides.manual ? '登记人工录入重量结果' : '登记 OCR 原值',
createResponse, `fileId=${fileId}`, SCENARIO_OPTIONS)
if (!result && !overrides.manual) {
createResponse = await context.request('POST', createPath, {
...payload, ocrRawText: null, ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
})
result = context.apiSuccess(MODULE, 'OCR 原值登记失败后建立可清理人工结果', createResponse,
`fileId=${fileId}`, NEW_CONTRACT_OPTIONS)
}
const resultId = id(result?.id)
context.assert(MODULE, '重量结果 ID、文件 ID 和初始版本契约正确', createResponse,
Boolean(resultId) && typeof result?.id === 'string' && typeof result?.fileId === 'string'
&& id(result?.fileId) === fileId && Number(result?.version) === 0,
`resultIdType=${typeof result?.id}, fileIdType=${typeof result?.fileId}, version=${result?.version ?? 'missing'}`,
SCENARIO_OPTIONS)
if (!resultId) throw new Error(`文件 ${fileId} 已上传但重量结果登记失败,需要人工清理该测试文件`)
return { upload: recognized, fileId, result, resultId, version: Number(result.version), deleted: false }
}
async function verifyImages(context, target, item, image) {
const base = `/api/platform/v1/files/measurements/${encodeURIComponent(target.tenantId)}/${encodeURIComponent(target.inboundId)}/${encodeURIComponent(item.fileId)}`
const original = await context.requestBinary('GET', base)
context.assert(MODULE, '鉴权读取原始称重图片', original,
original.http === 200 && original.bytes.length === image.bytes.length
&& /^image\/(jpeg|png|bmp)/i.test(original.contentType),
`contentType=${original.contentType || 'missing'}, bytes=${original.bytes.length}`, SCENARIO_OPTIONS)
const thumbnail = await context.requestBinary('GET', `${base}/thumbnail`)
const dimensions = jpegDimensions(thumbnail.bytes)
context.assert(MODULE, '缩略图返回受限尺寸 JPEG', thumbnail,
thumbnail.http === 200 && /^image\/jpeg/i.test(thumbnail.contentType)
&& thumbnail.bytes.length > 0 && dimensions && dimensions.width <= 480 && dimensions.height <= 320,
`contentType=${thumbnail.contentType || 'missing'}, bytes=${thumbnail.bytes.length}, dimensions=${dimensions ? `${dimensions.width}x${dimensions.height}` : 'unreadable'}`,
NEW_CONTRACT_OPTIONS)
}
async function loadWorkspace(context, target, name) {
const response = await context.request('GET', workspacePath(target.tenantId, target.inboundId))
const workspace = context.apiSuccess(MODULE, name, response, `inboundId=${target.inboundId}`, SCENARIO_OPTIONS)
return { response, workspace }
}
async function rejectWithoutMutation(context, target, item, name, payload) {
const path = confirmPath(target.tenantId, target.inboundId, item.resultId)
const response = await context.request('POST', path, payload)
context.assert(MODULE, name, response, expectedRejection(response),
`HTTP=${response.http}, code=${response.body?.code ?? 'missing'}`, SCENARIO_OPTIONS)
const check = await loadWorkspace(context, target, `${name}后回查工作区`)
const persisted = check.workspace?.weightResults?.find((row) => id(row.id) === item.resultId)
context.assert(MODULE, `${name}未改变重量结果`, check.response,
persisted && Number(persisted.version) === item.version && persisted.recognitionStatus !== 'CONFIRMED',
`version=${persisted?.version ?? 'missing'}, status=${persisted?.recognitionStatus ?? 'missing'}`, SCENARIO_OPTIONS)
}
async function deleteResult(context, target, item, name, options = NEW_CONTRACT_OPTIONS) {
const path = deletePath(target.tenantId, target.inboundId, item.resultId, item.version)
const response = await context.request('DELETE', path)
const passed = apiOk(response)
context.addStep(MODULE, name, response, passed,
`resultId=${item.resultId}, version=${item.version}`, options)
if (passed) item.deleted = true
return { response, passed }
}
async function verifyDeleted(context, target, item, baselineWeight) {
const check = await loadWorkspace(context, target, '删除后回查重量工作区')
const missing = !check.workspace?.weightResults?.some((row) => id(row.id) === item.resultId)
context.assert(MODULE, '删除后结果消失且总毛重恢复基线', check.response,
missing && (baselineWeight == null
? check.workspace?.actualGrossWeightKg == null
: closeNumber(check.workspace?.actualGrossWeightKg, baselineWeight)),
`resultMissing=${missing}, baseline=${baselineWeight ?? 'null'}, actual=${check.workspace?.actualGrossWeightKg ?? 'null'}`,
NEW_CONTRACT_OPTIONS)
const imagePath = `/api/platform/v1/files/measurements/${encodeURIComponent(target.tenantId)}/${encodeURIComponent(target.inboundId)}/${encodeURIComponent(item.fileId)}`
const image = await context.requestBinary('GET', imagePath)
context.assert(MODULE, '删除结果后原始图片不可再读取', image,
image.http === 200 && image.body && String(image.body.code) !== '200',
`HTTP=${image.http}, code=${image.body?.code ?? 'missing'}`, NEW_CONTRACT_OPTIONS)
}
export async function run(context) {
const scenarioStart = context.steps.length
const created = []
const target = await resolveMarkedInbound(context)
context.assert(MODULE, '仅选择带本轮测试标记的可编辑入库单', target?.response || null,
Boolean(target), target
? `tenantId=${target.tenantId}, inboundId=${target.inboundId}, status=${target.detail.summary.processStatus}`
: `marker=${context.marker}, 未找到本轮创建且状态为 ${[...EDITABLE_STATUSES].join('/')} 的入库单`,
SCENARIO_OPTIONS)
if (!target) {
context.scenarios.push({
name: SCENARIO, module: MODULE, success: false, deploymentBlocked: false,
passed: 0, failed: 1,
})
return
}
context.context.inboundWeightOcr = {
tenantId: target.tenantId, inboundId: target.inboundId, marker: context.marker,
}
context.createdResources.inboundWeightOcr = {
tenantId: target.tenantId, inboundId: target.inboundId, resultIds: [], cleaned: false,
}
const imagePath = process.env.ADMIN_TEST_OCR_IMAGE || DEFAULT_IMAGE
const imageBytes = await readFile(imagePath)
const image = { bytes: imageBytes, name: basename(imagePath), contentType: 'image/jpeg' }
const initial = await loadWorkspace(context, target, '读取 OCR 测试前重量工作区')
if (!initial.workspace) throw new Error('无法读取测试入库单重量工作区')
const baselineWeight = initial.workspace.actualGrossWeightKg
const baselineIds = new Set((initial.workspace.weightResults || []).map((row) => id(row.id)))
context.assert(MODULE, '测试入库单身份与基线可追踪', initial.response,
id(initial.workspace.inboundId) === target.inboundId
&& initial.workspace.processStatus === target.detail.summary.processStatus,
`existingResults=${baselineIds.size}, baselineWeight=${baselineWeight ?? 'null'}`, SCENARIO_OPTIONS)
const uploadPath = `/api/platform/v1/files/measurements/${encodeURIComponent(target.tenantId)}/${encodeURIComponent(target.inboundId)}`
const noAuth = await multipart(context, uploadPath, image, '')
context.assert(MODULE, '未认证请求不能上传称重图片', noAuth,
noAuth.http === 401 || noAuth.http === 403,
`HTTP=${noAuth.http}, code=${noAuth.body?.code ?? 'none'}`, SCENARIO_OPTIONS)
const wrongType = await multipart(context, uploadPath, {
bytes: new TextEncoder().encode(context.marker), name: `${context.marker}.txt`, contentType: 'text/plain',
})
context.assert(MODULE, '非图片类型被拒绝且不模拟成功', wrongType,
expectedRejection(wrongType), `HTTP=${wrongType.http}, code=${wrongType.body?.code ?? 'missing'}`, SCENARIO_OPTIONS)
const oversized = await multipart(context, uploadPath, {
bytes: new Uint8Array(5 * 1024 * 1024 + 1), name: `${context.marker}.jpg`, contentType: 'image/jpeg',
})
context.assert(MODULE, '超过5MB图片被拒绝', oversized,
expectedRejection(oversized), `HTTP=${oversized.http}, code=${oversized.body?.code ?? 'missing'}`, SCENARIO_OPTIONS)
try {
const original = await createUploadedResult(context, target, image, { retry: true })
created.push(original)
context.createdResources.inboundWeightOcr.resultIds.push(original.resultId)
await verifyImages(context, target, original, image)
const registered = await loadWorkspace(context, target, '登记后回查 OCR 重量结果')
const registeredRow = registered.workspace?.weightResults?.find((row) => id(row.id) === original.resultId)
context.assert(MODULE, '工作区保留本轮 OCR 原值且未覆盖历史结果', registered.response,
registeredRow && !baselineIds.has(original.resultId)
&& closeNumber(registeredRow.ocrWeightValue, original.result.ocrWeightValue)
&& registeredRow.ocrWeightUnit === original.result.ocrWeightUnit,
`resultFound=${Boolean(registeredRow)}, baselineCount=${baselineIds.size}, currentCount=${registered.workspace?.weightResults?.length ?? 'n/a'}`,
SCENARIO_OPTIONS)
const staleConfirm = await context.request('POST',
confirmPath(target.tenantId, target.inboundId, original.resultId),
{ version: original.version + 1 })
context.assert(MODULE, '旧版本确认返回并发冲突', staleConfirm, expectedConflict(staleConfirm),
`HTTP=${staleConfirm.http}, code=${staleConfirm.body?.code ?? 'missing'}`, NEW_CONTRACT_OPTIONS)
const recognizedValue = Number(original.result.ocrWeightValue)
const recognizedUnit = original.result.ocrWeightUnit
const canConfirmOriginal = Number.isFinite(recognizedValue) && recognizedValue > 0
&& ['KG', 'KGS', 'LB', 'LBS'].includes(String(recognizedUnit || '').trim().toUpperCase())
const confirmPayload = canConfirmOriginal
? { version: original.version }
: {
version: original.version, adjustedWeightValue: 1.234, adjustedWeightUnit: 'KG',
adjustmentReason: `${context.marker} OCR未返回有效重量,人工验收`,
}
const confirmResponse = await context.request('POST',
confirmPath(target.tenantId, target.inboundId, original.resultId), confirmPayload)
const confirmed = context.apiSuccess(MODULE, '确认 OCR 重量并计入实际总毛重', confirmResponse,
`usedOriginal=${canConfirmOriginal}`, NEW_CONTRACT_OPTIONS)
context.assert(MODULE, '确认结果状态、版本和最终KG正确', confirmResponse,
confirmed?.recognitionStatus === 'CONFIRMED' && Number(confirmed?.version) === original.version + 1
&& Number(confirmed?.finalWeightKg) > 0,
`status=${confirmed?.recognitionStatus ?? 'missing'}, version=${confirmed?.version ?? 'missing'}, finalKg=${confirmed?.finalWeightKg ?? 'missing'}`,
NEW_CONTRACT_OPTIONS)
original.version = Number(confirmed?.version ?? original.version)
const afterConfirm = await loadWorkspace(context, target, '确认后回查实际总毛重')
const expectedTotal = Number(baselineWeight || 0) + Number(confirmed?.finalWeightKg || 0)
context.assert(MODULE, '确认后实际总毛重按服务端结果重算', afterConfirm.response,
closeNumber(afterConfirm.workspace?.actualGrossWeightKg, expectedTotal),
`baseline=${baselineWeight ?? 'null'}, added=${confirmed?.finalWeightKg ?? 'missing'}, actual=${afterConfirm.workspace?.actualGrossWeightKg ?? 'missing'}`,
NEW_CONTRACT_OPTIONS)
const staleDelete = await context.request('DELETE',
deletePath(target.tenantId, target.inboundId, original.resultId, Math.max(0, original.version - 1)))
context.assert(MODULE, '旧版本删除返回并发冲突', staleDelete, expectedConflict(staleDelete),
`HTTP=${staleDelete.http}, code=${staleDelete.body?.code ?? 'missing'}`, NEW_CONTRACT_OPTIONS)
const deletedOriginal = await deleteResult(context, target, original, '删除本轮已确认图片及重量结果')
if (deletedOriginal.passed) await verifyDeleted(context, target, original, baselineWeight)
const manual = await createUploadedResult(context, target, image, { manual: true })
created.push(manual)
context.createdResources.inboundWeightOcr.resultIds.push(manual.resultId)
await rejectWithoutMutation(context, target, manual, '零重量确认被拒绝', {
version: manual.version, adjustedWeightValue: 0, adjustedWeightUnit: 'KG', adjustmentReason: context.marker,
})
await rejectWithoutMutation(context, target, manual, '负重量确认被拒绝', {
version: manual.version, adjustedWeightValue: -1, adjustedWeightUnit: 'KG', adjustmentReason: context.marker,
})
await rejectWithoutMutation(context, target, manual, 'LB 0.001换算为零后被拒绝', {
version: manual.version, adjustedWeightValue: 0.001, adjustedWeightUnit: 'LB', adjustmentReason: context.marker,
})
await rejectWithoutMutation(context, target, manual, '超出15位整数重量被拒绝', {
version: manual.version, adjustedWeightValue: 1_000_000_000_000_000,
adjustedWeightUnit: 'KG', adjustmentReason: context.marker,
})
const adjustedValue = 10.125
const adjustedConfirmResponse = await context.request('POST',
confirmPath(target.tenantId, target.inboundId, manual.resultId), {
version: manual.version, adjustedWeightValue: adjustedValue, adjustedWeightUnit: 'LB',
adjustmentReason: `${context.marker} 人工确认 LB`,
})
const adjusted = context.apiSuccess(MODULE, '人工改重与确认原子完成', adjustedConfirmResponse,
`adjusted=${adjustedValue} LB`, NEW_CONTRACT_OPTIONS)
const expectedKg = Math.round(adjustedValue * 0.45359237 * 1000) / 1000
context.assert(MODULE, 'LB 人工改重保留输入并正确换算最终KG', adjustedConfirmResponse,
adjusted?.recognitionStatus === 'CONFIRMED' && Number(adjusted?.version) === manual.version + 2
&& closeNumber(adjusted?.adjustedWeightValue, adjustedValue)
&& adjusted?.adjustedWeightUnit === 'LB' && closeNumber(adjusted?.finalWeightKg, expectedKg),
`adjusted=${adjusted?.adjustedWeightValue ?? 'missing'} ${adjusted?.adjustedWeightUnit ?? ''}, finalKg=${adjusted?.finalWeightKg ?? 'missing'}, expectedKg=${expectedKg}`,
NEW_CONTRACT_OPTIONS)
manual.version = Number(adjusted?.version ?? manual.version)
const deletedManual = await deleteResult(context, target, manual, '删除本轮人工改重图片及结果')
if (deletedManual.passed) await verifyDeleted(context, target, manual, baselineWeight)
} finally {
for (const item of created.filter((value) => !value.deleted)) {
await deleteResult(context, target, item, '异常收尾清理本轮重量结果', NEW_CONTRACT_OPTIONS)
}
const cleanup = await loadWorkspace(context, target, '最终清理后回查重量工作区')
const createdIds = new Set(created.map((item) => item.resultId))
const leftovers = (cleanup.workspace?.weightResults || []).filter((row) => createdIds.has(id(row.id)))
const restored = leftovers.length === 0 && (baselineWeight == null
? cleanup.workspace?.actualGrossWeightKg == null
: closeNumber(cleanup.workspace?.actualGrossWeightKg, baselineWeight))
context.assert(MODULE, '仅清理本轮结果且订单总毛重恢复运行前基线', cleanup.response,
restored, `leftovers=${leftovers.length}, baseline=${baselineWeight ?? 'null'}, actual=${cleanup.workspace?.actualGrossWeightKg ?? 'null'}`,
NEW_CONTRACT_OPTIONS)
context.createdResources.inboundWeightOcr.cleaned = restored
}
const steps = context.steps.slice(scenarioStart)
context.scenarios.push({
name: SCENARIO, module: MODULE, success: steps.every((step) => step.passed),
deploymentBlocked: steps.some((step) => step.deploymentBlocker),
passed: steps.filter((step) => step.passed).length,
failed: steps.filter((step) => !step.passed).length,
})
}