优化
This commit is contained in:
@@ -5,7 +5,10 @@ import test from 'node:test'
|
||||
import {
|
||||
confirmedNetSummary,
|
||||
normalizedGrossKg,
|
||||
palletOptionsPath,
|
||||
selectWeightPallet,
|
||||
WEIGHT_PALLET_OPTIONS_PAGE_SIZE,
|
||||
weightPalletOptionRecords,
|
||||
workspaceWeightMatchesBaseline,
|
||||
} from './inbound-weight-ocr.mjs'
|
||||
|
||||
@@ -42,7 +45,15 @@ test('cleanup restores a non-empty carton fallback when there were no confirmed
|
||||
|
||||
test('HTTP scenario uses pallet option and net weight contracts without legacy final weight', () => {
|
||||
const source = readFileSync(new URL('inbound-weight-ocr.mjs', import.meta.url), 'utf8')
|
||||
assert.match(source, /\/weight-pallet-options\?tenantId=/)
|
||||
const optionsPath = palletOptionsPath('10001', '20001', ' TP-09 ')
|
||||
assert.match(optionsPath, /\/weight-pallet-options\?tenantId=10001&(?:page=1&size=50|size=50&page=1)/)
|
||||
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,
|
||||
}), [{ palletId: '1' }])
|
||||
assert.equal(weightPalletOptionRecords([{ palletId: '1' }]), null)
|
||||
assert.equal(weightPalletOptionRecords({ records: [], page: 1, size: 1000 }), null)
|
||||
assert.ok(source.indexOf('const palletOptionsResponse') < source.lastIndexOf('const uploadPath'))
|
||||
assert.match(source, /form\.append\('file'/)
|
||||
assert.doesNotMatch(source, /form\.append\('palletId'/)
|
||||
|
||||
@@ -4,6 +4,7 @@ import process from 'node:process'
|
||||
|
||||
const MODULE = 'inbound-weight-ocr'
|
||||
const SCENARIO = 'weight-image-recognition-and-confirmation'
|
||||
export const WEIGHT_PALLET_OPTIONS_PAGE_SIZE = 50
|
||||
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'])
|
||||
@@ -202,8 +203,24 @@ function deletePath(tenantId, inboundId, resultId, version) {
|
||||
return `${resultPath(inboundId, resultId)}?tenantId=${encodeURIComponent(tenantId)}&version=${version}`
|
||||
}
|
||||
|
||||
function palletOptionsPath(tenantId, inboundId) {
|
||||
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/weight-pallet-options?tenantId=${encodeURIComponent(tenantId)}`
|
||||
export function palletOptionsPath(tenantId, inboundId, keyword = '') {
|
||||
const params = new URLSearchParams({
|
||||
tenantId: id(tenantId),
|
||||
page: '1',
|
||||
size: String(WEIGHT_PALLET_OPTIONS_PAGE_SIZE),
|
||||
})
|
||||
const normalizedKeyword = String(keyword || '').trim()
|
||||
if (normalizedKeyword) params.set('keyword', normalizedKeyword)
|
||||
return `/api/platform/v1/first-leg/inbounds/${encodeURIComponent(inboundId)}/weight-pallet-options?${params.toString()}`
|
||||
}
|
||||
|
||||
export function weightPalletOptionRecords(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
|
||||
}
|
||||
|
||||
async function createUploadedResult(context, target, pallet, image, overrides = {}) {
|
||||
@@ -382,13 +399,14 @@ export async function run(context) {
|
||||
const palletOptionsResponse = await context.request(
|
||||
'GET', palletOptionsPath(target.tenantId, target.inboundId),
|
||||
)
|
||||
const palletOptions = context.apiSuccess(MODULE, '读取同仓称重托盘候选', palletOptionsResponse,
|
||||
const palletOptionsPage = context.apiSuccess(MODULE, '分页读取同仓称重托盘候选', palletOptionsResponse,
|
||||
`inboundId=${target.inboundId}`, NEW_CONTRACT_OPTIONS)
|
||||
const palletOptions = weightPalletOptionRecords(palletOptionsPage)
|
||||
const palletOptionsComplete = Array.isArray(palletOptions) && palletOptions.length > 0
|
||||
&& palletOptions.every(palletSnapshotComplete)
|
||||
context.assert(MODULE, '托盘候选包含稳定快照字段且无需状态或占用资格', palletOptionsResponse,
|
||||
context.assert(MODULE, '托盘候选分页受控且包含稳定快照字段', palletOptionsResponse,
|
||||
palletOptionsComplete,
|
||||
`count=${Array.isArray(palletOptions) ? palletOptions.length : 'invalid'}, fields=palletId/palletCode/palletType/capacityCbm/tareWeightKg`,
|
||||
`page=${palletOptionsPage?.page ?? 'missing'}, size=${palletOptionsPage?.size ?? 'missing'}, count=${Array.isArray(palletOptions) ? palletOptions.length : 'invalid'}, maxSize=${WEIGHT_PALLET_OPTIONS_PAGE_SIZE}, fields=palletId/palletCode/palletType/capacityCbm/tareWeightKg`,
|
||||
NEW_CONTRACT_OPTIONS)
|
||||
const pallet = palletOptionsComplete ? selectWeightPallet(palletOptions) : null
|
||||
if (!pallet) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
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 service = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java')
|
||||
const warehouseClient = read('../../firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.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 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 contract marker: ${marker}`)
|
||||
const end = source.indexOf(nextMarker, start + marker.length)
|
||||
return source.slice(start, end === -1 ? source.length : end)
|
||||
}
|
||||
|
||||
test('backend Controller exposes a keyword-filtered bounded page instead of an unbounded array', () => {
|
||||
const block = endpointBlock(controller, '@GetMapping("/weight-pallet-options")', '@BusinessAudit')
|
||||
assert.match(block, /ApiResponse<PageResult<InboundMeasurementResponses\.PalletSnapshot>>/)
|
||||
assert.doesNotMatch(block, /ApiResponse<List<InboundMeasurementResponses\.PalletSnapshot>>/)
|
||||
assert.match(block, /\bpage\b/)
|
||||
assert.match(block, /\bsize\b/)
|
||||
assert.match(block, /\bkeyword\b/)
|
||||
|
||||
const boundedSize = [...block.matchAll(/@Max\((\d+)\)/g)].map((match) => Number(match[1]))
|
||||
assert.ok(boundedSize.some((maximum) => maximum > 0 && maximum <= 200),
|
||||
'size must have a finite Controller maximum no greater than 200')
|
||||
})
|
||||
|
||||
test('Firstleg forwards the bound to Warehouse instead of loading the full warehouse and slicing in memory', () => {
|
||||
const clientBlock = endpointBlock(warehouseClient, '@GetMapping("/pallets/snapshots")', '@PostMapping')
|
||||
assert.match(clientBlock, /ApiResponse<PageResult<PalletSnapshot>>/)
|
||||
assert.doesNotMatch(clientBlock, /ApiResponse<List<PalletSnapshot>>/)
|
||||
for (const parameter of ['page', 'size', 'keyword']) assert.match(clientBlock, new RegExp(`"${parameter}"`))
|
||||
|
||||
const serviceBlock = endpointBlock(service,
|
||||
'public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(',
|
||||
'@Transactional')
|
||||
assert.match(serviceBlock, /loadPalletSnapshotPage\(inbound, page, size, keyword\)/)
|
||||
assert.match(service, /palletSnapshots\([\s\S]{0,300}?page[\s\S]{0,80}?size[\s\S]{0,80}?keyword/)
|
||||
assert.doesNotMatch(serviceBlock, /\.stream\(\)[\s\S]*?\.skip\(/)
|
||||
})
|
||||
|
||||
test('OpenAPI publishes page, size, keyword and a finite maximum without an array data contract', () => {
|
||||
const block = endpointBlock(openapi,
|
||||
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-pallet-options:',
|
||||
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:')
|
||||
for (const parameter of ['page', 'size', 'keyword']) {
|
||||
assert.match(block, new RegExp(`name: ${parameter}`))
|
||||
}
|
||||
const maxima = [...block.matchAll(/maximum:\s*(\d+)/g)].map((match) => Number(match[1]))
|
||||
assert.ok(maxima.some((maximum) => maximum > 0 && maximum <= 200),
|
||||
'OpenAPI size parameter must declare a finite maximum no greater than 200')
|
||||
const dataLine = block.match(/data:\s*\{[^\n]+/)?.[0] || ''
|
||||
assert.doesNotMatch(dataLine, /type:\s*array/)
|
||||
assert.match(block, /(?:records|WeightPalletOptionPage|PageResult)/)
|
||||
})
|
||||
|
||||
test('Admin API requests a bounded remote-search page and does not type the response as an array', () => {
|
||||
const block = endpointBlock(adminApi, 'export async function getWeightPalletOptions', 'export async function uploadWeightImage')
|
||||
assert.match(block, /\bpage\b/)
|
||||
assert.match(block, /\bsize\b/)
|
||||
assert.match(block, /\bkeyword\b/)
|
||||
assert.doesNotMatch(block, /ApiEnvelope<WeightPalletOption\[\]>/)
|
||||
assert.match(block, /(?:records|PageResult|PageResource|PageResponse)/)
|
||||
})
|
||||
|
||||
test('Admin skips candidates without weight edit permission or an editable inbound state', () => {
|
||||
const loader = endpointBlock(adminPanel, 'async function loadPalletOptions', 'function chooseFile')
|
||||
const gate = endpointBlock(adminPanel, 'const canLoadPalletOptions', 'const virtualPalletOptions')
|
||||
assert.match(gate, /canEdit\.value/)
|
||||
assert.match(gate, /editable\.value/)
|
||||
assert.match(loader, /!canLoadPalletOptions\.value/)
|
||||
})
|
||||
|
||||
test('Admin behavior tests prove both permission and inbound-state request suppression', () => {
|
||||
const cases = [...adminBehavior.matchAll(/it\((['"`])([\s\S]*?)\1,\s*async\s*\(\)\s*=>\s*\{([\s\S]*?)\n\s*\}\)/g)]
|
||||
const suppressionCases = cases.filter(([, , title, body]) => /getWeightPalletOptions/.test(body)
|
||||
&& /not\.toHaveBeenCalled/.test(body))
|
||||
assert.ok(suppressionCases.some(([, , title]) => /edit|permission|权限/i.test(title)),
|
||||
'missing behavior case: no weight:edit must not request pallet candidates')
|
||||
assert.ok(suppressionCases.some(([, , title]) => /status|editable|状态/i.test(title)),
|
||||
'missing behavior case: non-editable inbound status must not request pallet candidates')
|
||||
})
|
||||
|
||||
test('Admin remote search is bounded and never accumulates every candidate', () => {
|
||||
assert.match(adminPanel, /\bremote\b/)
|
||||
assert.match(adminPanel, /:remote-method=/)
|
||||
const sizes = [...adminPanel.matchAll(/(?:PALLET|pallet).{0,40}(?:PAGE_SIZE|pageSize)\s*=\s*(\d+)/g)]
|
||||
.map((match) => Number(match[1]))
|
||||
assert.ok(sizes.some((size) => size > 0 && size <= 200),
|
||||
'Admin remote search page size must be finite and no greater than 200')
|
||||
assert.doesNotMatch(adminPanel, /palletOptions\.value\.(?:push|unshift)\s*\(/)
|
||||
assert.doesNotMatch(adminPanel, /palletOptions\.value\s*=\s*\[\s*\.\.\.palletOptions\.value/)
|
||||
assert.match(adminBehavior, /(?:remote search|远程搜索|keyword).{0,220}(?:size|page)/is)
|
||||
})
|
||||
|
||||
test('Admin uses a virtualized selector so a large response cannot create one DOM option per candidate', () => {
|
||||
assert.match(adminPanel, /<el-select-v2\b/)
|
||||
assert.doesNotMatch(adminPanel, /<el-option[\s\S]*?v-for="option in palletOptions"/)
|
||||
})
|
||||
|
||||
test('historical pallet snapshot remains read-only visible when it is no longer eligible', () => {
|
||||
const cards = endpointBlock(adminPanel, '<article v-for="(row, index) in rows"', '</article>')
|
||||
assert.match(cards, /row\.palletCode/)
|
||||
assert.match(cards, /row\.palletType/)
|
||||
assert.match(cards, /row\.palletTareWeightKg/)
|
||||
assert.match(adminBehavior, /(?:historical|history|历史|失去候选|no longer eligible).{0,220}(?:palletCode|托盘编码|TP-)/is)
|
||||
})
|
||||
Reference in New Issue
Block a user