From b797af706a9e2fea9995e32a86435540c43cd7b9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 01:27:44 +0800 Subject: [PATCH 1/4] test(admin): sync fee item baseline --- .../fee-items-contract.test.mjs | 9 +++++---- scripts/admin-test-env-modules/fee-items.mjs | 17 +++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/scripts/admin-test-env-modules/fee-items-contract.test.mjs b/scripts/admin-test-env-modules/fee-items-contract.test.mjs index 87eced03c..41d120019 100644 --- a/scripts/admin-test-env-modules/fee-items-contract.test.mjs +++ b/scripts/admin-test-env-modules/fee-items-contract.test.mjs @@ -31,13 +31,14 @@ test('stale cleanup only recognizes the acceptance-owned code and name pair', () test('baseline checks run only after stale acceptance cleanup', () => { assert.ok(source.indexOf('cleanupStaleAcceptanceItems(context)') - < source.indexOf("'正式目录111条且默认20条对应6页'")) + < source.indexOf("'正式目录112条且默认20条对应6页'")) }) -test('formal baseline includes the system FBA freight item in addition to the 110 controlled items', () => { - assert.match(source, /const BASELINE_COUNT = 111/) - assert.match(source, /const INTERNATIONAL_COUNT = 27/) +test('formal baseline includes both system fee items in addition to the 110 controlled items', () => { + assert.match(source, /const BASELINE_COUNT = 112/) + assert.match(source, /const INTERNATIONAL_COUNT = 28/) assert.match(source, /'FBA_HEAD_FREIGHT'/) + assert.match(source, /'PRIVATE_LAST_MILE_DELIVERY'/) }) test('fee-items injects its same-marker merchant and order fixtures for focused execution', () => { diff --git a/scripts/admin-test-env-modules/fee-items.mjs b/scripts/admin-test-env-modules/fee-items.mjs index b15d2d4bd..2778893b7 100644 --- a/scripts/admin-test-env-modules/fee-items.mjs +++ b/scripts/admin-test-env-modules/fee-items.mjs @@ -2,9 +2,9 @@ const MODULE = 'fee-items' const SCENARIO = 'platform-public-fee-items' const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } const BASE = '/api/platform/v1/fee-items' -const BASELINE_COUNT = 111 +const BASELINE_COUNT = 112 const DOMESTIC_COUNT = 84 -const INTERNATIONAL_COUNT = 27 +const INTERNATIONAL_COUNT = 28 const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || '' const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || '' @@ -47,6 +47,7 @@ function payload(suffix, overrides = {}) { function expectedCodes() { return [ 'FBA_HEAD_FREIGHT', + 'PRIVATE_LAST_MILE_DELIVERY', ...Array.from({ length: 110 }, (_, index) => `FEE_TYPE_${String(index + 1).padStart(3, '0')}`), ].sort() } @@ -90,7 +91,7 @@ export async function run(context) { const initialPageResponse = await context.request('GET', query({ page: 1, size: 20 })) const initialPage = context.apiSuccess(MODULE, '费用项目查看权限与分页查询', initialPageResponse, 'page=1,size=20', OPTIONS) - context.assert(MODULE, '正式目录111条且默认20条对应6页', initialPageResponse, + context.assert(MODULE, '正式目录112条且默认20条对应6页', initialPageResponse, records(initialPage).length === 20 && Number(initialPage?.total) === BASELINE_COUNT && Number(initialPage?.pages) === 6 && Number(initialPage?.page) === 1, `records=${records(initialPage).length}, total=${initialPage?.total ?? 'n/a'}, pages=${initialPage?.pages ?? 'n/a'}`, OPTIONS) @@ -103,13 +104,13 @@ export async function run(context) { const domestic = initialRows.filter((row) => row.tradeScope === 'DOMESTIC') const international = initialRows.filter((row) => row.tradeScope === 'INTERNATIONAL') const actualCodes = initialRows.map((row) => row.feeCode).sort() - context.assert(MODULE, '正式目录111条全部属于平台且状态ACTIVE', initial.response, + context.assert(MODULE, '正式目录112条全部属于平台且状态ACTIVE', initial.response, initialRows.length === BASELINE_COUNT && initialRows.every((row) => id(row.tenantId) === '0' && row.status === 'ACTIVE'), `rows=${initialRows.length}, platform=${initialRows.filter((row) => id(row.tenantId) === '0').length}, active=${initialRows.filter((row) => row.status === 'ACTIVE').length}`, OPTIONS) - context.assert(MODULE, '国内84条、国际27条范围正确', initial.response, + context.assert(MODULE, '国内84条、国际28条范围正确', initial.response, domestic.length === DOMESTIC_COUNT && international.length === INTERNATIONAL_COUNT, `DOMESTIC=${domestic.length}, INTERNATIONAL=${international.length}`, OPTIONS) - context.assert(MODULE, '正式编码完整覆盖110条受控项目及系统头程运费', initial.response, + context.assert(MODULE, '正式编码完整覆盖110条受控项目及两条系统费用', initial.response, actualCodes.join(',') === expectedCodes().join(','), `codes=${actualCodes.length}, first=${actualCodes[0] || 'missing'}, last=${actualCodes.at(-1) || 'missing'}`, OPTIONS) @@ -141,7 +142,7 @@ export async function run(context) { const activeResponse = await context.request('GET', query({ status: 'ACTIVE', page: 1, size: 200 })) const activePage = context.apiSuccess(MODULE, '按 ACTIVE 状态筛选费用项目', activeResponse, '', OPTIONS) - context.assert(MODULE, 'ACTIVE 筛选返回111条且无停用记录', activeResponse, + context.assert(MODULE, 'ACTIVE 筛选返回112条且无停用记录', activeResponse, Number(activePage?.total) === BASELINE_COUNT && records(activePage).every((row) => row.status === 'ACTIVE'), `total=${activePage?.total ?? 'n/a'}`, OPTIONS) @@ -272,7 +273,7 @@ export async function run(context) { `feeItemId=${feeItemId}`, OPTIONS) } const final = await listAll(context) - context.assert(MODULE, '清理后恢复111条正式基线且无临时编码或名称', final.response, + context.assert(MODULE, '清理后恢复112条正式基线且无临时编码或名称', final.response, final.rows.length === BASELINE_COUNT && !final.rows.some((row) => row.feeCode === createPayload.feeCode || String(row.feeTypeName).includes(suffix)), `rows=${final.rows.length}, temporary=${final.rows.filter((row) => row.feeCode === createPayload.feeCode).length}`, OPTIONS) From 3cfd45f4bb18b5a1f4a0d6e738a922cd6e047667 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 01:29:38 +0800 Subject: [PATCH 2/4] fix(lastleg): preserve generated pallet FBA destination --- .../ContainerInboundPlanPreviewResponse.java | 1 + ...ainerInboundPlanConversionCoordinator.java | 2 +- .../service/InboundPlanService.java | 17 +++++--- .../service/LoadingCompletedConsumer.java | 3 +- .../openapi/overseas-business-v1.yaml | 1 + ...rInboundPlanConversionCoordinatorTest.java | 41 +++++++++++++++++++ .../ContainerInboundPlanPreviewTest.java | 21 ++++++++++ .../service/LoadingCompletedConsumerTest.java | 4 +- 8 files changed, 82 insertions(+), 8 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ContainerInboundPlanPreviewResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ContainerInboundPlanPreviewResponse.java index af7d1d59f..145aacd90 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ContainerInboundPlanPreviewResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ContainerInboundPlanPreviewResponse.java @@ -48,6 +48,7 @@ public record ContainerInboundPlanPreviewResponse( String destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, + String fbaWarehouseCode, int packageCount, BigDecimal volumeCbm, Integer suggestedPalletCount, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java index 9ed4a0f3d..b78c9b395 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java @@ -210,7 +210,7 @@ public class ContainerInboundPlanConversionCoordinator { for (int localSequence = 1; localSequence <= order.suggestedPalletCount(); localSequence++) { items.add(new PreparedItem(order.customerId(), Long.parseLong(order.orderId()), planId, planNo, localSequence, ++globalSequence, "CIP:" + generationId + ":" + globalSequence, - "FBA".equals(order.destinationType()) ? order.destinationWarehouseCode() : null)); + trimToNull(order.fbaWarehouseCode()))); } } Preparation preparation = new Preparation(warehouseId, List.copyOf(items)); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java index bae48b5c6..f28059c3a 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java @@ -224,7 +224,7 @@ public class InboundPlanService { if (warehouseId == null || !Objects.equals(warehouseId, trimToNull(row.overseasWarehouseId()))) { blockers.add("RECEIVING_WAREHOUSE_INCONSISTENT"); } - boolean fba = "FBA".equals(destinationType(row)); + boolean fba = isFbaDestination(row); if (fba ? trimToNull(row.fbaWarehouseCode()) == null : row.destinationCount() != 1 || trimToNull(row.destinationWarehouseId()) == null && trimToNull(row.destinationWarehouseCode()) == null) blockers.add("DESTINATION_NOT_READY"); @@ -234,13 +234,14 @@ public class InboundPlanService { Integer suggested = suggestedPalletCount(row, blockers); if (!planned && (suggested == null || suggested <= 0)) blockers.add("PALLET_COUNT_NOT_READY"); String destinationType = destinationType(row); - String destinationCode = "FBA".equals(destinationType) && trimToNull(row.fbaWarehouseCode()) != null + String fbaWarehouseCode = isFbaDestination(row) ? trimToNull(row.fbaWarehouseCode()) : null; + String destinationCode = fbaWarehouseCode != null ? row.fbaWarehouseCode().trim() : row.destinationWarehouseCode(); String readiness = planned ? "PLANNED" : blockers.isEmpty() ? "READY" : "BLOCKED"; return new ContainerInboundPlanPreviewResponse.Order(row.customerId(), row.customerName(), String.valueOf(row.orderId()), trimToNull(row.orderNo()) == null ? row.sourceOrderNo() : row.orderNo(), row.inboundCode(), row.orderType(), destinationType, row.destinationWarehouseId(), - destinationCode, row.destinationWarehouseName(), row.packageCount(), row.volumeCbm(), + destinationCode, row.destinationWarehouseName(), fbaWarehouseCode, row.packageCount(), row.volumeCbm(), suggested, row.plannedPalletCount(), row.planNo(), row.planStatus(), row.version(), readiness, List.copyOf(blockers)); } @@ -258,7 +259,7 @@ public class InboundPlanService { || row.packageCount() > 0 && row.volumeCbm().signum() == 0) return null; try { return PalletAllocator.allocate(List.of(new PalletAllocator.LineInput(row.orderId(), - nullSafe(row.destinationWarehouseId()) + "|" + nullSafe("FBA".equals(destinationType(row)) + nullSafe(row.destinationWarehouseId()) + "|" + nullSafe(isFbaDestination(row) ? row.fbaWarehouseCode() : row.destinationWarehouseCode()), row.volumeCbm(), row.packageCount(), row.recommendedPalletCount()))).pallets().size(); } catch (IllegalArgumentException exception) { @@ -268,10 +269,16 @@ public class InboundPlanService { private static String destinationType(InboundPlanOrderMapper.ContainerPreviewOrderRow row) { if ("ONE_CLICK_DROPSHIP".equals(row.orderType())) return "ONE_CLICK_DROPSHIP"; - if ("FBA".equals(row.destinationType()) || trimToNull(row.fbaWarehouseCode()) != null) return "FBA"; + if (isFbaDestination(row)) return "FBA"; return "PRIVATE_ADDRESS"; } + private static boolean isFbaDestination(InboundPlanOrderMapper.ContainerPreviewOrderRow row) { + String explicit = trimToNull(row.destinationType()); + if (explicit != null) return "FBA".equalsIgnoreCase(explicit); + return trimToNull(row.fbaWarehouseCode()) != null; + } + private static int typeRank(String type) { return switch (type) { case "FBA" -> 0; case "ONE_CLICK_DROPSHIP" -> 1; default -> 2; }; } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java index e4ceb686b..47aadc593 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java @@ -247,7 +247,8 @@ public class LoadingCompletedConsumer { order.setOverseasWarehouseCode(DEFAULT_RECEIVING_WAREHOUSE_CODE); order.setOverseasWarehouseName(DEFAULT_RECEIVING_WAREHOUSE_NAME); order.setReceivingWarehouseVersion(0); - order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(containerNo); + order.setFbaWarehouseCode("FBA".equals(destinationType) ? source.destinationWarehouseCode() : null); + order.setContainerNo(containerNo); order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false); order.setPackageCount(source.packageCount()); order.setPalletCount("ONE_CLICK_DROPSHIP".equals(orderType) diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 7f185ee30..f630ae99e 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -2358,6 +2358,7 @@ components: destinationWarehouseId: { type: string, nullable: true } destinationWarehouseCode: { type: string, nullable: true } destinationWarehouseName: { type: string, nullable: true } + fbaWarehouseCode: { type: string, nullable: true, description: 底层目的类型为 FBA 时的目的仓编码;一件代发展示分类不改变该事实 } packageCount: { type: integer } volumeCbm: { type: number } suggestedPalletCount: { type: integer, nullable: true } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java index 0e8b2736d..c24c3f086 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java @@ -225,6 +225,47 @@ class ContainerInboundPlanConversionCoordinatorTest { verify(warehouse).confirmPalletBatch(any()); } + @Test + void preparationCarriesUnderlyingFbaCodeForDropshipWithoutClassifyingOverseasDropshipAsFba() { + ConvertContainerInboundPlanRequest request = request(); + var fba = previewOrder("100", "11", "RK-FBA", "ABE3", "ABE3"); + var overseas = previewOrder("200", "12", "RK-OVERSEAS", "US-LAX-01", null); + var preview = new ContainerInboundPlanPreviewResponse(BOOKING_ID, "MSCU6639870", "31", "US-LAX-01", + "Los Angeles", 7L, SNAPSHOT_HASH, 2, 2, 0, 2, 2, 0, 2, 0, true, + List.of(new ContainerInboundPlanPreviewResponse.Group("ONE_CLICK_DROPSHIP", null, "ABE3", "Amazon ABE3", + 1, 1, 1, 0, 1, 0, List.of(fba)), + new ContainerInboundPlanPreviewResponse.Group("ONE_CLICK_DROPSHIP", "31", "US-LAX-01", "Los Angeles", + 1, 1, 1, 0, 1, 0, List.of(overseas)))); + when(plans.previewContainer(BOOKING_ID)).thenReturn(preview); + when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class))) + .thenReturn("IP-FBA", "IP-OVERSEAS"); + + ContainerPlanConversionCommand command = ReflectionTestUtils.invokeMethod(coordinator, "prepareCommand", + 1L, 2L, BOOKING_ID, "FIRST-KEY", request, + ReflectionTestUtils.invokeMethod(coordinator, "hash", + List.of(BOOKING_ID, request.snapshotVersion(), request.snapshotHash()))); + ContainerInboundPlanConversionCoordinator.Preparation preparation; + try { + preparation = objectMapper.readValue(command.getPreparationJson(), + ContainerInboundPlanConversionCoordinator.Preparation.class); + } catch (Exception exception) { + throw new AssertionError(exception); + } + + assertEquals(java.util.Arrays.asList("ABE3", null), preparation.items().stream() + .map(ContainerInboundPlanConversionCoordinator.PreparedItem::fbaWarehouseCode).toList()); + } + + private ContainerInboundPlanPreviewResponse.Order previewOrder(String customerId, String orderId, + String inboundCode, String destinationCode, + String fbaWarehouseCode) { + return new ContainerInboundPlanPreviewResponse.Order(customerId, "Customer " + customerId, orderId, + "LM-" + orderId, inboundCode, "ONE_CLICK_DROPSHIP", "ONE_CLICK_DROPSHIP", + fbaWarehouseCode == null ? "31" : null, destinationCode, + fbaWarehouseCode == null ? "Los Angeles" : "Amazon " + destinationCode, + fbaWarehouseCode, 10, BigDecimal.ONE, 1, null, null, null, 0, "READY", List.of()); + } + private ConvertContainerInboundPlanRequest request() { return new ConvertContainerInboundPlanRequest(BOOKING_ID, 7L, SNAPSHOT_HASH); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanPreviewTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanPreviewTest.java index b5186dec5..77dfac1c4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanPreviewTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanPreviewTest.java @@ -2,6 +2,7 @@ package com.baiyue.lastleg.inboundplan.service; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot; +import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.BeforeEach; @@ -82,6 +83,26 @@ class ContainerInboundPlanPreviewTest { assertFalse(result.ready()); } + @Test + void dropshipPreviewKeepsUnderlyingFbaFactSeparateFromItsDisplayCategory() { + when(orderMapper.containerPreviewOrders("BOOKING-DROPSHIP")).thenReturn(List.of( + row(1L, "LM-FBA", "ONE_CLICK_DROPSHIP", "FBA", "ABE3", "Amazon ABE3", 1, null, null, 1), + row(2L, "LM-OVERSEAS", "ONE_CLICK_DROPSHIP", "OVERSEAS", "US-LAX-01", "洛杉矶仓", 1, null, null, 2) + )); + when(dropshipOrders.palletSnapshots("100", 1L)).thenReturn(List.of( + new DropshipPalletSnapshot("SRC-P1", 10, List.of()))); + when(dropshipOrders.palletSnapshots("100", 2L)).thenReturn(List.of( + new DropshipPalletSnapshot("SRC-P2", 10, List.of()))); + + var result = service.previewContainer("BOOKING-DROPSHIP"); + + assertEquals(java.util.Arrays.asList("ABE3", null), result.groups().stream() + .flatMap(group -> group.orders().stream()) + .map(ContainerInboundPlanPreviewResponse.Order::fbaWarehouseCode).toList()); + assertEquals(List.of("ONE_CLICK_DROPSHIP", "ONE_CLICK_DROPSHIP"), result.groups().stream() + .map(ContainerInboundPlanPreviewResponse.Group::destinationType).toList()); + } + private InboundPlanOrderMapper.ContainerPreviewOrderRow row( long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName, int recommendedPallets, String planNo, Integer plannedPallets, int version) { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java index 4fa5b1133..54f964f57 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java @@ -67,7 +67,9 @@ class LoadingCompletedConsumerTest { source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(), 2L,source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(2,service.consume(v2)); - verify(orders,times(2)).insert(any(LastlegOrder.class)); + ArgumentCaptor orderCaptor=ArgumentCaptor.forClass(LastlegOrder.class); + verify(orders,times(2)).insert(orderCaptor.capture()); + assertTrue(orderCaptor.getAllValues().stream().allMatch(order->order.getFbaWarehouseCode()==null)); ArgumentCaptor cartonCaptor= ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton.class); verify(cartons,times(8)).insert(cartonCaptor.capture()); From a0b1c761b78370909d43d82626dbc063f36051f3 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 01:33:03 +0800 Subject: [PATCH 3/4] fix(firstleg): complete inbound fee deployment schema --- deploy/sql/头程数据库统一增量-MySQL8.sql | 32 +++++++++++++++++++ .../initialization-baseline-contract.test.mjs | 8 +++++ 2 files changed, 40 insertions(+) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index e40a0998d..f3281f667 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3139,6 +3139,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee ( actual_weight_kg DECIMAL(18,3) NOT NULL COMMENT '试算时实际重量kg', actual_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '试算时实际体积m3', converted_quantity DECIMAL(18,4) NOT NULL COMMENT '换算后的重量或体积', + baseline_chargeable_quantity DECIMAL(18,6) NOT NULL COMMENT '系统按权威测量与生效报价计算的原始计费量', + tolerance_percent DECIMAL(5,2) NOT NULL DEFAULT 10.00 COMMENT '人工调整允许的最大偏差百分比,边界包含', chargeable_quantity DECIMAL(18,4) NOT NULL COMMENT '最终计费量', charge_unit VARCHAR(16) NOT NULL COMMENT '计费单位', currency CHAR(3) NOT NULL COMMENT '币种', @@ -3172,6 +3174,32 @@ ALTER TABLE fl_inbound_fee MODIFY COLUMN customer_id BIGINT UNSIGNED NULL COMMENT '可选业务客户快照;平台公共渠道计费不依赖该字段'; +SET @fee_baseline_quantity_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() + AND table_name='fl_inbound_fee' AND column_name='baseline_chargeable_quantity'), + 'SELECT 1', + 'ALTER TABLE fl_inbound_fee ADD COLUMN baseline_chargeable_quantity DECIMAL(18,6) NULL COMMENT ''系统按权威测量与生效报价计算的原始计费量'' AFTER converted_quantity' +); +PREPARE fee_baseline_quantity_stmt FROM @fee_baseline_quantity_sql; +EXECUTE fee_baseline_quantity_stmt; +DEALLOCATE PREPARE fee_baseline_quantity_stmt; +UPDATE fl_inbound_fee + SET baseline_chargeable_quantity = chargeable_quantity + WHERE baseline_chargeable_quantity IS NULL; +ALTER TABLE fl_inbound_fee + MODIFY COLUMN baseline_chargeable_quantity DECIMAL(18,6) NOT NULL + COMMENT '系统按权威测量与生效报价计算的原始计费量'; + +SET @fee_tolerance_percent_sql = IF( + EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() + AND table_name='fl_inbound_fee' AND column_name='tolerance_percent'), + 'SELECT 1', + 'ALTER TABLE fl_inbound_fee ADD COLUMN tolerance_percent DECIMAL(5,2) NOT NULL DEFAULT 10.00 COMMENT ''人工调整允许的最大偏差百分比,边界包含'' AFTER baseline_chargeable_quantity' +); +PREPARE fee_tolerance_percent_stmt FROM @fee_tolerance_percent_sql; +EXECUTE fee_tolerance_percent_stmt; +DEALLOCATE PREPARE fee_tolerance_percent_stmt; + CREATE TABLE IF NOT EXISTS fl_inbound_fee_item ( id BIGINT UNSIGNED NOT NULL COMMENT '费用明细ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', @@ -4433,6 +4461,8 @@ CALL unified_assert_column('baiyue_firstleg', 'fl_carrier_fba_rate', 'destinatio CALL unified_assert_column('baiyue_firstleg', 'fl_carrier_fba_rate', 'destination_warehouse_id'); CALL unified_assert_column('baiyue_firstleg', 'fl_carrier_fba_rate', 'destination_name'); CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_fee', 'price_book_version_id'); +CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_fee', 'baseline_chargeable_quantity'); +CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_fee', 'tolerance_percent'); CALL unified_assert_column('baiyue_file', 'file_export_task', 'execution_deadline_at'); DROP PROCEDURE IF EXISTS unified_assert_table; @@ -13470,6 +13500,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'actual_volume_cb CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'actual_weight_kg', '试算时实际重量kg'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'additional_amount', '附加费金额'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'business_type', '业务类型快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'baseline_chargeable_quantity', '系统按权威测量与生效报价计算的原始计费量'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'calculated_at', '试算时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'calculated_by', '试算人ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'carrier_id', '历史承运商ID;渠道价格可为空'); @@ -13518,6 +13549,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'sent_by', '发 CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'subtotal_amount', '基础费用金额'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'tax_mode', '订单买单或退税模式快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'tolerance_percent', '人工调整允许的最大偏差百分比,边界包含'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'total_amount', '总金额'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'updated_at', '更新时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'actual_gross_weight_kg', '仓库实测毛重快照,单位千克'); diff --git a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs index 0286e057a..2bc64aff1 100644 --- a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs +++ b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs @@ -50,6 +50,14 @@ test('国内统一初始化支持真实空库且不越界创建海外业务库', ) }) +test('国内统一初始化补齐入库费用人工调整基线字段', () => { + assert.match(sql, /baseline_chargeable_quantity DECIMAL\(18,6\) NOT NULL/) + assert.match(sql, /SET baseline_chargeable_quantity = chargeable_quantity[\s\S]*?WHERE baseline_chargeable_quantity IS NULL/) + assert.match(sql, /tolerance_percent DECIMAL\(5,2\) NOT NULL DEFAULT 10\.00/) + assert.match(sql, /unified_assert_column\('baiyue_firstleg', 'fl_inbound_fee', 'baseline_chargeable_quantity'\)/) + assert.match(sql, /unified_assert_column\('baiyue_firstleg', 'fl_inbound_fee', 'tolerance_percent'\)/) +}) + test('三期权限迁移删除退役权限并只保留 canonical 权限', () => { for (const source of [sql, oceanBookingSchema]) { assert.match(source, /DELETE FROM iam_permission WHERE id IN \(5102141,5102143,5102144,5102145\)/) From 859fba4ecce9e235489f6b87f004cae5c60d92ca Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 01:46:11 +0800 Subject: [PATCH 4/4] test(admin): confirm fee adjustment tolerance --- .../admin-test-env-modules/inbound-fees-contract.test.mjs | 5 +++++ scripts/admin-test-env-modules/inbound-fees.mjs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs index d1b4fe621..0aca65035 100644 --- a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs @@ -23,3 +23,8 @@ test('inbound fee lifecycle covers frozen source, version, publication and WAIT_ assert.match(source, /对客合计仅含已发布头程与已审核其他应收且其他应付单列/) assert.doesNotMatch(source, /settlementIncluded|是否结算|参与结算/) }) + +test('automatic fee adjustment confirms the ten percent tolerance boundary', async () => { + const source = await readFile(moduleFile, 'utf8') + assert.match(source, /reason: `\$\{context\.marker\}-自动费用版本调整`, toleranceConfirmed: true/) +}) diff --git a/scripts/admin-test-env-modules/inbound-fees.mjs b/scripts/admin-test-env-modules/inbound-fees.mjs index a27ad73ee..2d35c0b4c 100644 --- a/scripts/admin-test-env-modules/inbound-fees.mjs +++ b/scripts/admin-test-env-modules/inbound-fees.mjs @@ -72,7 +72,7 @@ export async function run(context) { `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/published?tenantId=${encodeURIComponent(tenantId)}`, { expectedFeeVersion: Number(initial.feeVersion), chargeableQuantity: adjustedQuantity, unitPrice: adjustedUnitPrice, additionalAmount: adjustedAdditional, - reason: `${context.marker}-自动费用版本调整` }, undefined, + reason: `${context.marker}-自动费用版本调整`, toleranceConfirmed: true }, undefined, requestHeaders(`${context.marker}-inbound-fee-adjust`)) const adjusted = context.apiSuccess(MODULE, '调整初算并生成新版本', adjustResponse, '', OPTIONS) context.assert(MODULE, '调整版本仍为 CALCULATED 且不阻塞 WAIT_SPLIT', adjustResponse,