This commit is contained in:
2026-08-27 17:16:33 +08:00
parent 61a97cd057
commit 76c24867a4
19 changed files with 474 additions and 107 deletions
@@ -1,4 +1,5 @@
export const CONTAINER_CONVERT_ENDPOINT = '/api/platform/v1/last-leg/inbound-plans/convert'
export const CONTAINER_SUMMARY_ENDPOINT = '/api/platform/v1/last-leg/orders/container-summaries'
export const CONTAINER_CONVERT_ENDPOINT = '/api/platform/v1/last-leg/inbound-plans/convert-container'
export const CONTAINER_CONVERT_EXPECTED_ORDERS = 15
export function groupOrdersByCustomer(orders) {
@@ -13,7 +14,7 @@ export function groupOrdersByCustomer(orders) {
}
export function validContainerBatchResult(result, sourceOrders) {
const plans = Array.isArray(result?.plans) ? result.plans : []
const plans = Array.isArray(result) ? result : []
if (!Array.isArray(sourceOrders) || !sourceOrders.length || plans.length !== sourceOrders.length) return false
const sourceIds = new Set(sourceOrders.map((order) => String(order.orderId)))
const resultIds = plans.flatMap((plan) => Array.isArray(plan?.lines) ? plan.lines : [])
@@ -32,15 +33,14 @@ export function validContainerBatchResult(result, sourceOrders) {
})
}
export function validOrderFact(actual, expected) {
const exact = (field) => (actual?.[field] ?? null) === (expected?.[field] ?? null)
return ['packageCount', 'volumeCbm', 'productQuantity', 'volumeWeightKg',
'palletCount', 'estimatedArrivalAt', 'actualArrivalAt', 'estimatedWarehouseAt'].every(exact)
}
export function distinctPhysicalPalletCount(rows) {
if (!Array.isArray(rows)) return 0
return new Set(rows.map((row) => String(row?.physicalPalletId ?? '').trim()).filter(Boolean)).size
export function validContainerSummaryPage(data, expected) {
if (!Array.isArray(data?.records)) return false
const row = data.records.find((item) => String(item?.sourceContainerBookingId ?? '')
=== String(expected?.sourceContainerBookingId ?? ''))
return Number(data.page) >= 1 && Number(data.size) >= 1 && Number(data.total) >= 1
&& Number(data.pages) >= 1 && String(row?.containerNo ?? '') === String(expected?.containerNo ?? '')
&& Number(row?.orderCount) === Number(expected?.orderCount)
&& Number(row?.inboundCodeCount) === Number(expected?.inboundCodeCount)
}
export function zeroDrift(before, after) {
@@ -50,9 +50,3 @@ export function zeroDrift(before, after) {
export function stableReplay(first, replay) {
return zeroDrift(first, replay)
}
export function validPdaCandidate(candidate, expected) {
return String(candidate?.containerNo ?? '') === String(expected?.containerNo ?? '')
&& Number(candidate?.planCount) === Number(expected?.planCount)
&& Number(candidate?.inboundCodeCount) === Number(expected?.inboundCodeCount)
}
@@ -1,9 +1,9 @@
import {
CONTAINER_CONVERT_ENDPOINT,
CONTAINER_CONVERT_EXPECTED_ORDERS,
CONTAINER_SUMMARY_ENDPOINT,
validContainerBatchResult,
validOrderFact,
validPdaCandidate,
validContainerSummaryPage,
zeroDrift,
} from './container-inbound-plan-batch-contract.mjs'
@@ -15,15 +15,15 @@ const success = (response) => response?.http === 200 && String(response?.body?.c
const trace = (response) => `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
const rejected = (response, codes) => response?.http === 200 && codes.includes(String(response?.body?.code))
function gap(context, name, reason, priority = 'P0') {
context.addCoverageGap(MODULE, MODULE, name, reason, 'fixture', { deploymentBlocker: priority === 'P0' })
function gap(context, name, reason, priority = 'P1') {
context.addCoverageGap(MODULE, MODULE, name, reason, 'fixture', { deploymentBlocker: true })
context.addStep(MODULE, name, null, false, `${priority}: ${reason}`, OPTIONS)
throw new Error(`${priority} blocking coverage gap: ${name}: ${reason}`)
}
function fixture(context) {
const value = context.createdResources?.phase4ContainerInboundPlanFixture
if (value?.marker !== context.marker || !value?.normal?.containerNo
if (value?.marker !== context.marker || !value?.normal?.containerNo || !value?.normal?.sourceContainerBookingId
|| !Array.isArray(value.normal.orders) || value.normal.orders.length !== CONTAINER_CONVERT_EXPECTED_ORDERS) {
gap(context, '整柜15票新数据前置', '缺少本轮marker的15票同柜正常fixture,禁止借用历史已转换数据')
}
@@ -32,7 +32,6 @@ function fixture(context) {
gap(context, `${name}整柜前置`, `缺少本轮${name}专用fixture,不能验证整批零漂移`, 'P1')
}
}
if (!value.pdaToken) gap(context, 'PDA候选Token前置', '缺少绑定真实海外仓的WAREHOUSE_PICKER海外Token')
return value
}
@@ -63,32 +62,33 @@ async function snapshot(context, source) {
}
function convertBody(source) {
const anchor = source.orders[0]
return { orderId: id(anchor.orderId), customerId: id(anchor.customerId) }
return { sourceContainerBookingId: id(source.sourceContainerBookingId) }
}
async function convert(context, name, source, key) {
const response = await context.requestOverseas('POST', CONTAINER_CONVERT_ENDPOINT,
convertBody(source), {}, context.overseasToken, { 'Idempotency-Key': key })
convertBody(source), { 'Idempotency-Key': key }, context.overseasToken)
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
return response
}
export async function run(context) {
const data = fixture(context)
const summaryResponse = await context.requestOverseas('GET',
`${CONTAINER_SUMMARY_ENDPOINT}?page=1&size=10&containerNo=${encodeURIComponent(data.normal.containerNo)}`)
context.assert(MODULE, 'P1服务端柜级摘要跨订单分页仍返回15票', summaryResponse,
success(summaryResponse) && validContainerSummaryPage(summaryResponse.body?.data, {
sourceContainerBookingId: data.normal.sourceContainerBookingId,
containerNo: data.normal.containerNo,
orderCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
inboundCodeCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
}), `orderPageSize=10, expectedOrderCount=15; ${trace(summaryResponse)}`, OPTIONS)
const normalList = await listOrders(context, data.normal.containerNo)
context.assert(MODULE, 'P0同柜15票列表前置', normalList.response,
context.assert(MODULE, 'P1同柜15票列表前置', normalList.response,
success(normalList.response) && normalList.rows.length === CONTAINER_CONVERT_EXPECTED_ORDERS
&& normalList.rows.every((row) => typeof row.id === 'string' && typeof row.customerId === 'string'),
`orders=${normalList.rows.length}; ${trace(normalList.response)}`, OPTIONS)
for (const expected of data.normal.orders) {
const actual = normalList.rows.find((row) => id(row.id) === id(expected.orderId))
context.assert(MODULE, `P2订单事实不猜算:${id(expected.orderId)}`, normalList.response,
Boolean(actual) && validOrderFact(actual, expected),
`customerId=${id(expected.customerId)}, inboundCode=${expected.inboundCode || 'missing'}`, { ...OPTIONS, deploymentBlocker: false })
}
const invalidBefore = await snapshot(context, data.invalid)
const invalid = await convert(context, 'P1任一非法票整柜转换拒绝', data.invalid, `${context.marker}-invalid-container`)
const invalidAfter = await snapshot(context, data.invalid)
@@ -103,9 +103,9 @@ export async function run(context) {
rejected(partial, ['A0409']) && zeroDrift(partialBefore, partialAfter), trace(partial), OPTIONS)
const key = `${context.marker}-container-convert`
const first = await convert(context, 'P0整柜一次转换并逐订单生成独立计划', data.normal, key)
const first = await convert(context, 'P1整柜一次转换并逐订单生成独立计划', data.normal, key)
const result = first?.body?.data
context.assert(MODULE, 'P0整柜响应覆盖全部订单和客户计划', first,
context.assert(MODULE, 'P1整柜响应覆盖全部订单和独立计划', first,
success(first) && validContainerBatchResult(result, data.normal.orders), trace(first), OPTIONS)
const replay = await convert(context, 'P1同键同请求重放', data.normal, key)
@@ -122,30 +122,9 @@ export async function run(context) {
concurrent.map(trace).join('; '), OPTIONS)
const after = await listOrders(context, data.normal.containerNo)
context.assert(MODULE, 'P0全部15票推进INBOUND_PLANNED', after.response,
context.assert(MODULE, 'P1全部15票推进INBOUND_PLANNED', after.response,
success(after.response) && after.rows.length === CONTAINER_CONVERT_EXPECTED_ORDERS
&& after.rows.every((row) => row.currentStatus === 'INBOUND_PLANNED' && row.inboundOrderNo),
`planned=${after.rows.filter((row) => row.currentStatus === 'INBOUND_PLANNED').length}; ${trace(after.response)}`, OPTIONS)
const candidateResponse = await context.requestOverseas('GET',
'/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, data.pdaToken)
const candidate = Array.isArray(candidateResponse?.body?.data)
? candidateResponse.body.data.find((row) => row?.containerNo === data.normal.containerNo) : null
context.assert(MODULE, 'P0同柜PDA候选计划数与入仓码数一致', candidateResponse,
success(candidateResponse) && validPdaCandidate(candidate, {
containerNo: data.normal.containerNo,
planCount: data.normal.orders.length,
inboundCodeCount: data.normal.orders.length,
}), trace(candidateResponse), OPTIONS)
const carton = data.normal.orders.flatMap((order) => order.cartonMarks || [])
.map((cartonMark) => ({ cartonMark })).at(0)
if (!carton) gap(context, 'P3箱码命中fixture', '正常整柜没有可用于resolve的冻结箱码', 'P3')
const prepared = data.preparedSession
if (!prepared) gap(context, 'P3箱码命中会话', '需先由PDA合同完成prepare+三照片+sign,再执行resolve', 'P3')
const resolved = await context.requestOverseas('POST',
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', carton, {}, data.pdaToken)
context.assert(MODULE, 'P3箱码服务端命中当前柜唯一入仓码', resolved,
success(resolved) && data.normal.orders.some((order) => order.inboundCode === resolved?.body?.data?.inboundCode),
trace(resolved), { ...OPTIONS, deploymentBlocker: false })
}
@@ -5,12 +5,11 @@ import test from 'node:test'
import {
CONTAINER_CONVERT_ENDPOINT,
CONTAINER_CONVERT_EXPECTED_ORDERS,
distinctPhysicalPalletCount,
CONTAINER_SUMMARY_ENDPOINT,
groupOrdersByCustomer,
stableReplay,
validContainerBatchResult,
validOrderFact,
validPdaCandidate,
validContainerSummaryPage,
zeroDrift,
} from './container-inbound-plan-batch-contract.mjs'
@@ -19,7 +18,6 @@ const scenario = read('phase4-container-inbound-plan-batch.mjs')
const controller = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java')
const service = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java')
const openapi = read('../../lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml')
const pda = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java')
const adminApi = read('../../../apps/admin-web/src/modules/overseas-warehouse/api/lastMileOrders.ts')
const adminView = read('../../../apps/admin-web/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue')
@@ -36,35 +34,32 @@ test('semantic contract creates one independent plan per order even for the same
const source = orders()
assert.equal(CONTAINER_CONVERT_EXPECTED_ORDERS, 15)
assert.equal(groupOrdersByCustomer(source).size, 2)
const result = { plans: source.map((order, index) => ({
const result = source.map((order, index) => ({
customerId: order.customerId, planNo: `LIP-${index + 1}`, status: 'PENDING_ASSIGNMENT',
lines: [{ orderId: order.orderId }], pallets: [{}], allocations: [{}],
})) }
}))
assert.equal(validContainerBatchResult(result, source), true)
})
test('facts preserve source values and physical pallets are not counted once per SKU', () => {
const expected = orders()[0]
assert.equal(validOrderFact({ ...expected }, expected), true)
assert.equal(validOrderFact({ ...expected, actualArrivalAt: '2026-09-02T08:00:00' }, expected), false)
assert.equal(distinctPhysicalPalletCount([
{ physicalPalletId: 'P1', sku: 'A' }, { physicalPalletId: 'P1', sku: 'B' }, { physicalPalletId: 'P2', sku: 'A' },
]), 2)
test('server container summary remains complete when the order page would contain only ten rows', () => {
assert.equal(CONTAINER_SUMMARY_ENDPOINT, '/api/platform/v1/last-leg/orders/container-summaries')
assert.equal(validContainerSummaryPage({
records: [{ sourceContainerBookingId: 'CB-1', containerNo: 'C1', orderCount: '15', inboundCodeCount: '15' }],
page: '1', size: '10', total: '1', pages: '1',
}, { sourceContainerBookingId: 'CB-1', containerNo: 'C1', orderCount: 15, inboundCodeCount: 15 }), true)
})
test('atomic failure, replay and PDA count helpers reject drift', () => {
test('atomic failure and replay helpers reject drift', () => {
const snapshot = { orders: [{ id: '1', status: 'PENDING_ASSIGNMENT' }], plans: [] }
assert.equal(zeroDrift(snapshot, structuredClone(snapshot)), true)
assert.equal(stableReplay({ plans: [{ planNo: 'LIP-1' }] }, { plans: [{ planNo: 'LIP-1' }] }), true)
assert.equal(validPdaCandidate({ containerNo: 'C1', planCount: '2', inboundCodeCount: '15' },
{ containerNo: 'C1', planCount: 2, inboundCodeCount: 15 }), true)
})
test('HTTP scenario freezes normal, invalid, replay, concurrency, partial and PDA evidence', () => {
assert.equal(CONTAINER_CONVERT_ENDPOINT, '/api/platform/v1/last-leg/inbound-plans/convert')
for (const marker of ['P0整柜一次转换并逐订单生成独立计划', 'P1任一非法票整柜转换拒绝',
assert.equal(CONTAINER_CONVERT_ENDPOINT, '/api/platform/v1/last-leg/inbound-plans/convert-container')
for (const marker of ['P1整柜一次转换并逐订单生成独立计划', 'P1任一非法票整柜转换拒绝',
'P1同键同请求重放', 'P1并发双击A', 'P1部分已转换整柜冲突',
'P0同柜PDA候选计划数与入仓码数一致', 'P3箱码服务端命中当前柜唯一入仓码']) {
'P1服务端柜级摘要跨订单分页仍返回15票']) {
assert.match(scenario, new RegExp(marker))
}
assert.match(scenario, /Promise\.all/)
@@ -73,17 +68,14 @@ test('HTTP scenario freezes normal, invalid, replay, concurrency, partial and PD
assert.match(scenario, /traceId/)
})
test('platform API keeps two long string identifiers and Admin uses the selected row customer', () => {
test('container API uses one stable long string identity', () => {
const endpoint = openapi.slice(openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`),
openapi.indexOf(' /api/platform/v1/last-leg/dispatches:', openapi.indexOf(` ${CONTAINER_CONVERT_ENDPOINT}:`)))
assert.match(endpoint, /required: \[customerId, orderId\]/)
assert.match(endpoint, /customerId:\s*\{ type: string/)
assert.match(endpoint, /orderId:\s*\{ type: string/)
assert.match(controller, /convertScope\(user, request\.customerId\(\)\)/)
assert.match(adminApi, /orderId: string[\s\S]{0,80}customerId: string/)
assert.match(adminView, /selectedRows\.value\.length !== 1/)
assert.match(adminView, /customerId = typeof selected\.customerId === 'string' \? selected\.customerId\.trim\(\)/)
assert.match(adminView, /orderId: String\(selected\.id\), customerId/)
assert.match(endpoint, /required: \[sourceContainerBookingId\]/)
assert.match(endpoint, /sourceContainerBookingId:\s*\{ type: string/)
assert.match(controller, /convertContainer/)
assert.match(adminApi, /sourceContainerBookingId: string/)
assert.match(adminView, /sourceContainerBookingId: row\.sourceContainerBookingId/)
})
test('backend batch conversion is one transaction and never merges same-customer orders', () => {
@@ -92,13 +84,14 @@ test('backend batch conversion is one transaction and never merges same-customer
assert.match(service, /(?:List<InboundPlanResponse>|Container.*Response|Batch.*Response)/)
assert.match(service, /(?:for \([^)]*(?:order|source)|\.map\([^)]*(?:order|source))/)
assert.doesNotMatch(service, /groupingBy\([^)]*(?:customerId|getCustomerId)/)
assert.match(service, /(?:partial|部分已转换|already belongs)[\s\S]{0,180}(?:CONFLICT|HttpStatus\.CONFLICT)/i)
assert.match(service, /来源柜存在不能转换的尾程订单/)
assert.match(service, /HttpStatus\.CONFLICT/)
})
test('PDA candidate and resolve contracts remain server-owned', () => {
assert.match(pda, /grouped\.computeIfAbsent\(plan\.getContainerNo\(\)/)
assert.match(pda, /entry\.getValue\(\)\.size\(\)/)
assert.match(pda, /totals\.inboundCount\(\)/)
assert.match(pda, /requireReceivingAvailable\(customerId, containerNo\)/)
assert.match(pda, /requireCartonRef|requireUnmatchedFbaCartonPrefix/)
test('Admin consumes a server-paged container summary and never derives 15 from the current order page', () => {
assert.match(adminApi, /(?:listLastMileContainerSummaries|listContainerSummaries)/)
assert.match(adminApi, /container-summaries/)
assert.match(adminView, /(?:listLastMileContainerSummaries|listContainerSummaries)\(/)
assert.match(adminView, /containerRows\.value\s*=\s*result\.records/)
assert.doesNotMatch(adminView, /(?:rows|result\.records)\.value?\.(?:reduce|filter|forEach)[\s\S]{0,300}containerNo/)
})
+25 -1
View File
@@ -232,7 +232,9 @@ Assert-Contains $navigationRecovery 'if (!isStaleRouteAssetError(error))' 'Admin
$domesticPdaRuntime = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/config/runtime.js')
$domesticPdaVite = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/vite.config.js')
$domesticPdaSession = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/stores/session.js')
Assert-Contains $domesticPdaRuntime "VITE_H5_API_BASE_URL || '/api'" '国内 PDA H5 同源 API'
Assert-Contains $domesticPdaRuntime "VITE_H5_API_BASE_URL || '/gateway-api'" '国内 PDA H5 同源 API'
Assert-Contains $domesticPdaVite "'/gateway-api'" '国内 PDA H5 独立代理前缀'
Assert-Contains $domesticPdaVite 'path.replace(/^\/gateway-api/, ''/api'')' '国内 PDA H5 Gateway 重写'
Assert-Contains $domesticPdaVite 'API_PROXY_TARGET' '国内 PDA H5 代理上游'
Assert-Contains $domesticPdaSession 'session.apiBase = PLATFORM_API_ROOT' '国内 PDA 当前构建 API'
Assert-NotContains $domesticPdaSession 'apiBase: session.apiBase' '国内 PDA 会话持久化'
@@ -303,6 +305,21 @@ Assert-Contains $productImageController "hasAuthority('platform:order:list')" '
Assert-Contains $receivingController '/current/unload-photos/files/{fileId}/download' '海外 PDA 签收照片预览'
Assert-Contains $receivingController 'CacheControl.noStore()' '海外 PDA 签收照片缓存边界'
# 待分配尾程订单按来源柜由服务端聚合,整柜转换每票独立计划且整批原子。
$lastlegBusinessOpenApi = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml')
$adminLastMileApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/overseas-warehouse/api/lastMileOrders.ts')
Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/orders/container-summaries:' 'Lastleg 柜级摘要契约'
Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/inbound-plans/convert-container:' 'Lastleg 整柜计划转换契约'
Assert-Contains $lastlegBusinessOpenApi 'sourceContainerBookingId' 'Lastleg 稳定来源柜身份'
Assert-Contains $adminAgents '/orders/container-summaries' 'Admin 柜级摘要目标入口'
Assert-Contains $adminAgents '/inbound-plans/convert-container' 'Admin 整柜转换目标入口'
Assert-Contains $adminLastMileApi '/orders/container-summaries' 'Admin 柜级摘要真实调用'
Assert-NotContains $adminLastMileApi '/orders/pending-containers' 'Admin 旧柜级摘要调用'
# 国内 PDA 部分拣货必须展示真实可用库位事实,并在无库位时保持页面整洁。
Assert-Contains $pdaAgents 'tests/partial-layout.test.mjs' '国内 PDA 部分拣货布局合同'
Assert-Contains $pdaAgents 'tests/picking-location-display.test.mjs' '国内 PDA 库位展示合同'
# Maven 聚合声明的全部历史/当前模块必须在后端根指引中可定位。
[xml]$pom = Get-Content -LiteralPath (Join-Path $backendRoot 'pom.xml') -Raw
foreach ($module in $pom.project.modules.module) {
@@ -312,6 +329,8 @@ foreach ($module in $pom.project.modules.module) {
# 对本轮高变化链路做精确文件门禁,防止改名或删除后留下悬空索引。
$criticalPaths = @(
'apps/admin-web/src/modules/overseas-warehouse/api/inboundOrders.ts',
'apps/admin-web/src/modules/overseas-warehouse/api/lastMileOrders.ts',
'apps/admin-web/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue',
'apps/admin-web/src/modules/overseas-warehouse/api/inboundPlans.ts',
'apps/admin-web/src/modules/overseas-warehouse/api/inventory.ts',
'apps/admin-web/src/modules/overseas-warehouse/api/pallets.ts',
@@ -351,6 +370,8 @@ $criticalPaths = @(
'apps/pda-uniapp/vite.config.js',
'apps/pda-uniapp/utils/app-update.js',
'apps/pda-uniapp/tests/app-update.test.mjs',
'apps/pda-uniapp/tests/partial-layout.test.mjs',
'apps/pda-uniapp/tests/picking-location-display.test.mjs',
'apps/pda-uniapp/utils/evidence-upload-queue.js',
'apps/pda-overseas-uniapp/package.json',
'apps/pda-overseas-uniapp/src/pages.json',
@@ -423,6 +444,9 @@ $criticalPaths = @(
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/security/service/LastlegSecurityProjectionService.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/security/service/LastlegSecurityValidationService.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dto/PendingContainerSummary.java',
'baiyue-firstleg/lastleg-service/src/test/java/com/baiyue/lastleg/service/PendingContainerSummaryContractTest.java',
'baiyue-firstleg/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/UnloadPhotoFileGateway.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/controller/PdaStocktakeController.java',