This commit is contained in:
2026-08-27 13:50:33 +08:00
parent daf0ee2acd
commit 3b38fdd33d
7 changed files with 117 additions and 12 deletions
@@ -4,6 +4,7 @@ import test from 'node:test'
import {
confirmedNetSummary,
normalizeWeightPalletOptionPage,
normalizedGrossKg,
palletOptionsPath,
selectWeightPallet,
@@ -50,10 +51,13 @@ test('HTTP scenario uses pallet option and net weight contracts without legacy f
assert.match(optionsPath, /keyword=TP-09/)
assert.equal(WEIGHT_PALLET_OPTIONS_PAGE_SIZE, 50)
assert.deepEqual(weightPalletOptionRecords({
records: [{ palletId: '1' }], page: 1, size: 50, total: 1000, pages: 20,
records: [{ palletId: '1' }], page: '1', size: '50', total: '1000', pages: '20',
}), [{ palletId: '1' }])
assert.deepEqual(normalizeWeightPalletOptionPage({
records: [], page: '1', size: '20', total: '0', pages: '0',
}), { records: [], page: 1, size: 20, total: 0, pages: 0 })
assert.equal(weightPalletOptionRecords([{ palletId: '1' }]), null)
assert.equal(weightPalletOptionRecords({ records: [], page: 1, size: 1000 }), null)
assert.equal(weightPalletOptionRecords({ records: [], page: 1, size: 1000, total: 0, pages: 0 }), null)
assert.ok(source.indexOf('const palletOptionsResponse') < source.lastIndexOf('const uploadPath'))
assert.match(source, /form\.append\('file'/)
assert.doesNotMatch(source, /form\.append\('palletId'/)
@@ -214,13 +214,21 @@ export function palletOptionsPath(tenantId, inboundId, keyword = '') {
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/weight-pallet-options?${params.toString()}`
}
export function weightPalletOptionRecords(page) {
export function normalizeWeightPalletOptionPage(page) {
if (!page || Array.isArray(page) || !Array.isArray(page.records)) return null
if (!Number.isInteger(Number(page.page)) || Number(page.page) < 1) return null
if (!Number.isInteger(Number(page.size)) || Number(page.size) < 1
|| Number(page.size) > WEIGHT_PALLET_OPTIONS_PAGE_SIZE) return null
if (page.records.length > Number(page.size)) return null
return page.records
const normalized = Object.fromEntries(['page', 'size', 'total', 'pages']
.map((key) => [key, Number(page[key])]))
if (!Number.isInteger(normalized.page) || normalized.page < 1) return null
if (!Number.isInteger(normalized.size) || normalized.size < 1
|| normalized.size > WEIGHT_PALLET_OPTIONS_PAGE_SIZE) return null
if (!Number.isInteger(normalized.total) || normalized.total < 0) return null
if (!Number.isInteger(normalized.pages) || normalized.pages < 0) return null
if (page.records.length > normalized.size) return null
return { records: page.records, ...normalized }
}
export function weightPalletOptionRecords(page) {
return normalizeWeightPalletOptionPage(page)?.records ?? null
}
async function createUploadedResult(context, target, pallet, image, overrides = {}) {
@@ -401,7 +409,12 @@ export async function run(context) {
)
const palletOptionsPage = context.apiSuccess(MODULE, '分页读取同仓称重托盘候选', palletOptionsResponse,
`inboundId=${target.inboundId}`, NEW_CONTRACT_OPTIONS)
const palletOptions = weightPalletOptionRecords(palletOptionsPage)
const normalizedPalletOptionsPage = normalizeWeightPalletOptionPage(palletOptionsPage)
const palletOptions = normalizedPalletOptionsPage?.records ?? null
context.assert(MODULE, '托盘候选成功响应为正式分页对象且数字字符串可归一化', palletOptionsResponse,
normalizedPalletOptionsPage !== null,
`page=${palletOptionsPage?.page ?? 'missing'}, size=${palletOptionsPage?.size ?? 'missing'}, total=${palletOptionsPage?.total ?? 'missing'}, pages=${palletOptionsPage?.pages ?? 'missing'}`,
NEW_CONTRACT_OPTIONS)
const palletOptionsComplete = Array.isArray(palletOptions) && palletOptions.length > 0
&& palletOptions.every(palletSnapshotComplete)
context.assert(MODULE, '托盘候选分页受控且包含稳定快照字段', palletOptionsResponse,
@@ -0,0 +1,86 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { normalizeWeightPalletOptionPage } from './inbound-weight-ocr.mjs'
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java')
const pageResult = read('../../common-core/src/main/java/com/baiyue/common/core/api/PageResult.java')
const longJackson = read('../../common-web/src/main/java/com/baiyue/common/web/LongIdJacksonConfiguration.java')
const openapi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml')
const adminApi = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.ts')
const adminApiTests = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.test.ts')
const adminPanel = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.vue')
const adminBehavior = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts')
function endpointBlock(source, marker, nextMarker) {
const start = source.indexOf(marker)
assert.notEqual(start, -1, `missing marker: ${marker}`)
const end = source.indexOf(nextMarker, start + marker.length)
return source.slice(start, end === -1 ? source.length : end)
}
test('formal success accepts two records and string pagination numbers', () => {
const records = [{ palletId: '1' }, { palletId: '2' }]
assert.deepEqual(normalizeWeightPalletOptionPage({ records, page: '1', size: '20', total: '2', pages: '1' }),
{ records, page: 1, size: 20, total: 2, pages: 1 })
})
test('formal empty page remains a successful page instead of an error', () => {
assert.deepEqual(normalizeWeightPalletOptionPage({ records: [], page: '1', size: '20', total: '0', pages: '0' }),
{ records: [], page: 1, size: 20, total: 0, pages: 0 })
})
test('malformed array and invalid page metadata are rejected by the HTTP contract', () => {
assert.equal(normalizeWeightPalletOptionPage([]), null)
assert.equal(normalizeWeightPalletOptionPage({ records: [], page: 1, size: 20, total: -1, pages: 0 }), null)
assert.equal(normalizeWeightPalletOptionPage({ records: [], page: 1, size: 101, total: 0, pages: 0 }), null)
})
test('Controller DTO and response envelope use PageResult records/page/size/total/pages', () => {
const block = endpointBlock(controller, '@GetMapping("/weight-pallet-options")', '@BusinessAudit')
assert.match(block, /ApiResponse<PageResult<InboundMeasurementResponses\.PalletSnapshot>>/)
assert.match(pageResult, /record PageResult<T>\(List<T> records, long page, long size, long total, long pages\)/)
})
test('OpenAPI documents the page object and permits runtime string pagination numbers', () => {
const endpoint = endpointBlock(openapi,
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-pallet-options:',
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:')
assert.match(endpoint, /data: \{\$ref: '#\/components\/schemas\/WeightPalletOptionPage'\}/)
const schema = endpointBlock(openapi, ' WeightPalletOptionPage:', ' CreateWeightResultRequest:')
assert.match(schema, /required: \[records, page, size, total, pages\]/)
for (const field of ['page', 'size', 'total', 'pages']) {
assert.match(schema, new RegExp(`${field}:[^\n]*(?:oneOf|string)`),
`${field} must permit the string form emitted by the shared Long serializer`)
}
assert.match(longJackson, /serializerByType\(Long\.TYPE, ToStringSerializer\.instance\)/)
})
test('Admin reads records, normalizes numeric strings, and never treats data as an array', () => {
const block = endpointBlock(adminApi, 'export async function getWeightPalletOptions', 'export async function uploadWeightImage')
assert.match(block, /ApiEnvelope<WeightPalletOptionPageResponse>/)
assert.match(block, /records:\s*page\.records/)
assert.match(block, /normalizedPageNumber\(page\.(?:page|size|total|pages)/)
assert.doesNotMatch(block, /ApiEnvelope<WeightPalletOption\[\]>/)
assert.match(adminPanel, /palletOptions\.value = result\.records/)
})
test('Admin behavior covers two-record success and an empty successful page', () => {
assert.match(adminApiTests, /formal paged pallet response[\s\S]{0,900}records[\s\S]{0,300}page: '1'/)
assert.match(adminBehavior, /no pallet options[\s\S]{0,500}palletPage\(\[\]\)/)
})
test('Admin behavior exposes a body code business error as a candidate load failure', () => {
assert.match(adminBehavior, /getWeightPalletOptions\)\.mockRejectedValueOnce\(new ApiResponseError\([^)]*'A0500'/)
assert.match(adminBehavior, /可选托盘加载失败|托盘服务暂不可用/)
})
test('Admin behavior separately covers a non-2xx HTTP or network error', () => {
assert.match(adminBehavior, /(?:HTTP error|HTTP错误|network error|网络错误)[\s\S]{0,700}(?:AxiosError|HTTP_5|status)/i)
})
test('Admin aborts and ignores an expired candidate response', () => {
assert.match(adminBehavior, /in-flight pallet request[\s\S]{0,900}signal\?\.aborted[\s\S]{0,300}过期托盘/)
})