diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java
index 964d961f6..111cc787b 100644
--- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java
+++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java
@@ -113,22 +113,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING')
)
-
- AND (
-
-
- o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING')
- o.process_status='WAIT_SPLIT'
- o.process_status='LOADING'
- 1=0
-
-
- )
-
- AND o.tenant_id=#{query.customerId}
- AND LOWER(o.inbound_code) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%')
- AND o.created_at>=#{query.createdStart}
- AND o.created_at<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)
) eligible
LEFT JOIN fl_coload_group_member current_member
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
@@ -143,6 +127,26 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1)
))
+
+ AND MAX(CASE WHEN
+
+
+ eligible.candidateStatus='WAIT_MEASURE'
+ eligible.candidateStatus='WAIT_SPLIT'
+ eligible.candidateStatus='LOADING'
+ 1=0
+
+
+ THEN 1 ELSE 0 END)=1
+
+
+ AND MAX(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)=1
+
+
+ AND MAX(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)=1
+
+ AND MIN(eligible.createdAt)>=#{query.createdStart}
+ AND MIN(eligible.createdAt)<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
""")
diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java
index a03e09762..447ee11f6 100644
--- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java
+++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java
@@ -97,8 +97,8 @@ class SmartLoadingCandidateMapperContractTest {
assertThat(sql)
.contains("o.created_at createdAt")
- .contains("o.created_at>=#{query.createdStart}")
- .contains("o.created_at<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)")
+ .contains("MIN(eligible.createdAt)>=#{query.createdStart}")
+ .contains("MIN(eligible.createdAt)<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)")
.doesNotContain("query.expectedArrivalStart", "query.expectedArrivalEnd");
}
@@ -136,9 +136,11 @@ class SmartLoadingCandidateMapperContractTest {
.getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql();
assertThat(renderedUnits)
.contains("o.channel_code IN")
- .contains("o.created_at>=", "o.created_at=", "MIN(eligible.createdAt) item?.evidenceType === evidenceType
+ && item?.status !== 'VOID')
+ if (!evidenceItem?.fileId) return false
}
return true
}
@@ -508,15 +535,34 @@ export async function run(context) {
let planId = id(plan?.id)
if (!planId) return
+ const candidateStartedAt = Date.now()
const candidateResponse = await context.request('GET',
- `/api/platform/v1/first-leg/smart-loading/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(first.warehouseId)}`)
- const candidates = context.apiSuccess(MODULE, '读取整票智能装柜候选', candidateResponse,
- evidence(candidateResponse), OPTIONS) || []
- const groupedCandidates = candidates.filter((item) => id(item?.coLoadGroupId) === groupId)
- context.assert(MODULE, '每个组成员候选携带相同整组事实', candidateResponse,
- groupedCandidates.length === 2 && groupedCandidates.every((item) => Number(item?.memberCount) === 2
- && Number(item?.groupTotalBoxes) === first.packages + second.packages && item?.locked !== true),
- `count=${groupedCandidates.length}, groupTotal=${first.packages + second.packages}`, OPTIONS)
+ `/api/platform/v1/first-leg/smart-loading/inbound-candidates?channelGroupCodes=${encodeURIComponent(channelGroup(first.channelCode))}&warehouseId=${encodeURIComponent(first.warehouseId)}&page=1&size=200`)
+ const candidateElapsedMs = Date.now() - candidateStartedAt
+ const candidatePage = context.apiSuccess(MODULE, '读取整票智能装柜候选', candidateResponse,
+ evidence(candidateResponse), OPTIONS)
+ const candidates = candidatePage?.records || []
+ context.assert(MODULE, '全局候选200条分页查询性能受控', candidateResponse,
+ candidateElapsedMs < 5000 && Number(candidatePage?.page) === 1 && Number(candidatePage?.size) === 200,
+ `elapsedMs=${candidateElapsedMs}, total=${candidatePage?.total ?? 'missing'}`, OPTIONS)
+
+ const today = new Date().toISOString().slice(0, 10)
+ const filteredResponse = await context.request('GET',
+ `${SMART}/inbound-candidates?channelGroupCodes=${encodeURIComponent(channelGroup(first.channelCode))}`
+ + `&warehouseId=${encodeURIComponent(first.warehouseId)}&inboundCode=${encodeURIComponent(first.inboundCode)}`
+ + `&createdStart=${today}&createdEnd=${today}&orderStatuses=PENDING_REVIEW&orderStatuses=WAIT_SPLIT&page=1&size=20`)
+ const filteredPage = context.apiSuccess(MODULE, '按创建日期和多状态查询同柜组候选', filteredResponse,
+ evidence(filteredResponse), OPTIONS)
+ const filteredGroup = (filteredPage?.records || []).find((item) => id(item?.coLoadGroupId) === groupId)
+ context.assert(MODULE, '同柜组作为一个分页单位携带全部成员事实', filteredResponse,
+ Number(filteredPage?.records?.length) === 1 && Number(filteredGroup?.memberCount) === 2
+ && Number(filteredGroup?.groupTotalBoxes) === first.packages + second.packages
+ && filteredGroup?.locked !== true && filteredGroup?.groupMembers?.length === 2,
+ `units=${filteredPage?.records?.length ?? 0}, members=${filteredGroup?.groupMembers?.length ?? 0}, groupTotal=${first.packages + second.packages}`, OPTIONS)
+ context.assert(MODULE, '多状态仅用于搜索且命中整组不拆页', filteredResponse,
+ filteredGroup?.orderStatusSnapshot === 'WAIT_SPLIT' && filteredGroup?.groupMembers?.length === 2,
+ `date=${today}, status=${filteredGroup?.orderStatusSnapshot || 'missing'}, members=${filteredGroup?.groupMembers?.length ?? 0}`,
+ OPTIONS)
let isolatedCandidates = candidates
const competing = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true)
@@ -524,14 +570,13 @@ export async function run(context) {
let isolatedCandidateResponse = candidateResponse
if (competing.length > 0) {
isolatedCandidateResponse = await context.request('GET',
- `${SMART}/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(first.warehouseId)}`)
- isolatedCandidates = context.apiSuccess(MODULE, '清理后重新隔离读取目标组候选', isolatedCandidateResponse,
- evidence(isolatedCandidateResponse), OPTIONS) || []
+ `${SMART}/inbound-candidates?channelGroupCodes=${encodeURIComponent(channelGroup(first.channelCode))}&warehouseId=${encodeURIComponent(first.warehouseId)}&page=1&size=200`)
+ const isolatedCandidatePage = context.apiSuccess(MODULE, '清理后重新隔离读取目标组候选', isolatedCandidateResponse,
+ evidence(isolatedCandidateResponse), OPTIONS)
+ isolatedCandidates = isolatedCandidatePage?.records || []
}
- const candidateGroupIds = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true)
- .map((item) => id(item?.coLoadGroupId)).filter(Boolean))]
- const targetCandidateInbounds = isolatedCandidates.filter((item) => id(item?.coLoadGroupId) === groupId)
- .map((item) => id(item?.inboundId)).sort()
+ const candidateGroupIds = filteredGroup ? [id(filteredGroup.coLoadGroupId)] : []
+ const targetCandidateInbounds = (filteredGroup?.groupMembers || []).map((item) => id(item?.inboundId)).sort()
context.assert(MODULE, '推荐候选包含当前目标同柜组且成员身份精确', isolatedCandidateResponse,
candidateGroupIds.includes(groupId)
&& JSON.stringify(targetCandidateInbounds) === JSON.stringify([first.inboundId, second.inboundId].sort()),
@@ -539,8 +584,8 @@ export async function run(context) {
const allCandidateVolume = isolatedCandidates
.reduce((sum, item) => sum + Number(item?.availableVolumeCbm || 0), 0)
context.assert(MODULE, '推荐候选总量形成容量竞争场景', isolatedCandidateResponse,
- allCandidateVolume > 33 * 1.1,
- `targetGroupId=${groupId}, candidateVolume=${allCandidateVolume}, limit=${33 * 1.1}`, OPTIONS)
+ allCandidateVolume > 33 * 1.5,
+ `targetGroupId=${groupId}, candidateVolume=${allCandidateVolume}, limit=${33 * 1.5}`, OPTIONS)
const beforeRecommend = plan
const recommendResponse = await context.request('POST',
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/recommend`, {
@@ -827,7 +872,7 @@ export async function run(context) {
fullBatch?.workflowStatus === 'AWAITING_REVIEW'
&& fullBatch?.items?.every((item) => Number(item?.pickedPackageCount) === Number(item?.plannedPackageCount)),
`workflow=${fullBatch?.workflowStatus || 'missing'}, items=${fullBatch?.items?.length ?? 0}`, OPTIONS)
- if (!await uploadCompletionEvidence(context, third, 'coload-full')) return
+ if (!await uploadCompletionEvidence(context, fullPda.taskId, third.warehouseId, 'coload-full')) return
const fullCompleteResponse = await context.request('POST', `${PDA}/tasks/${fullPda.taskId}/complete`, {
expectedTaskVersion: Number(fullBatch?.version),
operationId: `${context.marker}-coload-full-complete`.slice(0, 128), remark: context.marker,
@@ -870,7 +915,7 @@ export async function run(context) {
}, undefined, { 'Idempotency-Key': `${context.marker}-normal-batch`.slice(0, 128) })
const normalBatch = context.apiSuccess(MODULE, '未成组普通单PDA批量拣货不回归', normalBatchResponse,
evidence(normalBatchResponse), OPTIONS)
- if (!await uploadCompletionEvidence(context, seventh, 'normal-split')) return
+ if (!await uploadCompletionEvidence(context, normalPda.taskId, seventh.warehouseId, 'normal-split')) return
const normalCompleteResponse = await context.request('POST', `${PDA}/tasks/${normalPda.taskId}/complete`, {
expectedTaskVersion: Number(normalBatch?.version),
operationId: `${context.marker}-normal-complete`.slice(0, 128), remark: context.marker,
diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs
index 856c8594e..db9ca72ab 100644
--- a/scripts/admin-test-env-modules/inbound-measurement.mjs
+++ b/scripts/admin-test-env-modules/inbound-measurement.mjs
@@ -415,20 +415,31 @@ export async function run(context) {
let measuredResponse = await context.request('GET', measurementPath)
let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
- const weightedOuter = measurementProfile?.outer || { length: 64, width: 54, height: 54 }
- const unitVolume = halfUp(
- weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000, 3)
+ const weightedOuter = {
+ length: Number(measuredItem?.calculatedOuterLengthCm ?? measuredItem?.outerLengthCm),
+ width: Number(measuredItem?.calculatedOuterWidthCm ?? measuredItem?.outerWidthCm),
+ height: Number(measuredItem?.calculatedOuterHeightCm ?? measuredItem?.outerHeightCm),
+ }
+ const calculatedVolume = weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000
+ const usesMatchedAddRule = String(measuredItem?.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_')
+ const unitVolume = usesMatchedAddRule ? halfUp(calculatedVolume, 3) : ceiling(calculatedVolume, 4)
const totalVolume = unitVolume * originalPackageCount
const volumeWeight = ceiling(totalVolume * 167, 3)
+ // Device scans retain the measured girth while the weighted dimensions are
+ // used for volume. The subsequent manual draft explicitly keeps ADD so the
+ // same customer dimension rule remains frozen for review.
const girth = ceiling(replacement.length + 2 * (replacement.width + replacement.height), 3)
context.assert(MODULE, '覆盖尺寸、箱数和服务端体积公式正确', measuredResponse,
measuredItem && closeTo(measuredItem.lengthCm, replacement.length)
&& closeTo(measuredItem.widthCm, replacement.width) && closeTo(measuredItem.heightCm, replacement.height)
&& measuredItem.scannedShipmentId === replacementShipmentId
&& Number(measuredItem.confirmedPackageCount) === originalPackageCount
- && closeTo(measuredItem.outerLengthCm, weightedOuter.length)
- && closeTo(measuredItem.outerWidthCm, weightedOuter.width)
- && closeTo(measuredItem.outerHeightCm, weightedOuter.height)
+ && closeTo(measuredItem.calculatedOuterLengthCm, weightedOuter.length)
+ && closeTo(measuredItem.calculatedOuterWidthCm, weightedOuter.width)
+ && closeTo(measuredItem.calculatedOuterHeightCm, weightedOuter.height)
+ && (measuredItem.coefficientMode == null
+ || ['ACTUAL', 'ADD', 'MULTIPLY'].includes(measuredItem.coefficientMode))
+ && Boolean(measuredItem.calculationRuleVersion)
&& closeTo(measuredItem.unitVolumeCbm, unitVolume) && closeTo(measuredItem.totalVolumeCbm, totalVolume)
&& closeTo(measuredItem.volumeWeightKg, volumeWeight) && closeTo(measuredItem.girthCm, girth)
&& closeTo(measured.actualVolumeCbm, totalVolume) && closeTo(measured.volumeWeightKg, volumeWeight),
@@ -532,7 +543,10 @@ export async function run(context) {
intelligentImageFileId: item.intelligentImageFileId,
overLimitWarning: item.overLimitWarning ?? null,
outerLengthCm: item.outerLengthCm ?? null, outerWidthCm: item.outerWidthCm ?? null,
- outerHeightCm: item.outerHeightCm ?? null, remark: context.marker,
+ outerHeightCm: item.outerHeightCm ?? null,
+ coefficientMode: String(item.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_')
+ ? 'ADD' : item.coefficientMode ?? null,
+ remark: context.marker,
}))
: [{
inboundDetailId, itemNo: measuredItem.itemNo,
@@ -541,7 +555,10 @@ export async function run(context) {
intelligentImageFileId: measuredItem.intelligentImageFileId,
overLimitWarning: measuredItem.overLimitWarning ?? null,
outerLengthCm: measuredItem.outerLengthCm ?? null, outerWidthCm: measuredItem.outerWidthCm ?? null,
- outerHeightCm: measuredItem.outerHeightCm ?? null, remark: context.marker,
+ outerHeightCm: measuredItem.outerHeightCm ?? null,
+ coefficientMode: String(measuredItem.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_')
+ ? 'ADD' : measuredItem.coefficientMode ?? null,
+ remark: context.marker,
}]
const draftPayload = {
measurementId: measured.measurementId, version: Number(measured.version),