diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index ac44ef196..a1f23b92b 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -7681,6 +7681,15 @@ SELECT 193000000001000111, 0, 'FBA_HEAD_FREIGHT', '头程运费', 'INTERNATIONAL 0, 'system', '2026-08-14 00:00:00.000', 0, 'system', '2026-08-14 00:00:00.000' WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item WHERE fee_code = 'FBA_HEAD_FREIGHT'); +INSERT INTO fl_fee_item (id, tenant_id, fee_code, category, trade_scope, fee_type_name, + pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, + allocation_fee, description, status, created_by, created_by_name, created_at, + updated_by, updated_by_name, updated_at, deleted, version) +SELECT 193000000001000112, 0, 'PRIVATE_LAST_MILE_DELIVERY', '尾程派送费', 'INTERNATIONAL', + '私人仓尾程派送费', 0, 0, 1, 1, 0, '仅用于destinationType=PRIVATE的入库单', + 'ACTIVE', 0, 'SYSTEM', CURRENT_TIMESTAMP(3), 0, 'SYSTEM', CURRENT_TIMESTAMP(3), 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item WHERE fee_code = 'PRIVATE_LAST_MILE_DELIVERY'); + -- ============================================================================ -- 2026-08-06:三期分柜、预装、拣货、装柜和派送当前基线 -- ============================================================================ @@ -15607,6 +15616,11 @@ BEGIN AND category = '头程运费' AND trade_scope = 'INTERNATIONAL' AND fee_type_name = 'FBA头程运费' AND pickup_payable = 0 AND pickup_receivable = 0 AND warehouse_payable = 0 AND warehouse_receivable = 1 AND allocation_fee = 0 AND status = 'ACTIVE' AND deleted = 0) <> 1 + OR (SELECT COUNT(*) FROM baiyue_firstleg.fl_fee_item + WHERE tenant_id = 0 AND fee_code = 'PRIVATE_LAST_MILE_DELIVERY' + AND category = '尾程派送费' AND trade_scope = 'INTERNATIONAL' + AND fee_type_name = '私人仓尾程派送费' AND warehouse_payable = 1 + AND warehouse_receivable = 1 AND status = 'ACTIVE' AND deleted = 0) <> 1 OR EXISTS ( SELECT 1 FROM baiyue_firstleg.fl_inbound_fee_detail detail diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java index b35a25a7a..de61d512c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java @@ -23,6 +23,7 @@ import java.util.List; @Service public class InboundFeeDetailService { private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT"; + private static final String PRIVATE_LAST_MILE_DELIVERY_FEE_CODE = "PRIVATE_LAST_MILE_DELIVERY"; @Resource private FeeItemRepository repository; @Resource @@ -379,6 +380,7 @@ public class InboundFeeDetailService { private void apply(InboundFeeDetailEntity value, InboundOrderEntity inbound, FeeItemEntity item, SaveRequest request, long operatorId) { + ensureDestinationApplicable(item, inbound); ensureWarehouseApplicable(item, request.direction()); value.setFeeItemId(item.getId()); value.setInboundCode(inbound.getInboundCode()); @@ -408,6 +410,13 @@ public class InboundFeeDetailService { value.setUpdatedBy(operatorId); } + private static void ensureDestinationApplicable(FeeItemEntity item, InboundOrderEntity inbound) { + if (PRIVATE_LAST_MILE_DELIVERY_FEE_CODE.equals(item.getFeeCode()) + && !"PRIVATE".equals(inbound.getDestinationType())) { + throw conflict("私人仓尾程派送费只能绑定目的地类型为PRIVATE的入库单"); + } + } + private void applySupplierSnapshot(InboundFeeDetailEntity value, FeeItemEntity item, String direction, Long supplierId) { if (!"PAYABLE".equals(direction)) { diff --git a/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql b/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql index 4fa48c82d..0e115c876 100644 --- a/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql +++ b/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql @@ -471,6 +471,15 @@ SELECT 193000000001000111, 0, 'FBA_HEAD_FREIGHT', '头程运费', 'INTERNATIONAL 0, 'system', '2026-08-14 00:00:00.000', 0, 'system', '2026-08-14 00:00:00.000' WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item WHERE fee_code = 'FBA_HEAD_FREIGHT'); +INSERT INTO fl_fee_item (id, tenant_id, fee_code, category, trade_scope, fee_type_name, + pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, + allocation_fee, description, status, created_by, created_by_name, created_at, + updated_by, updated_by_name, updated_at, deleted, version) +SELECT 193000000001000112, 0, 'PRIVATE_LAST_MILE_DELIVERY', '尾程派送费', 'INTERNATIONAL', + '私人仓尾程派送费', 0, 0, 1, 1, 0, '仅用于destinationType=PRIVATE的入库单', + 'ACTIVE', 0, 'SYSTEM', CURRENT_TIMESTAMP(3), 0, 'SYSTEM', CURRENT_TIMESTAMP(3), 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item WHERE fee_code = 'PRIVATE_LAST_MILE_DELIVERY'); + DROP PROCEDURE IF EXISTS phase2_publish_assert_fee_schema; DELIMITER $$ CREATE PROCEDURE phase2_publish_assert_fee_schema() @@ -491,11 +500,15 @@ BEGIN OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND status = 'ACTIVE' AND deleted = 0 AND id BETWEEN 193000000001000001 AND 193000000001000110 AND fee_code = CONCAT('FEE_TYPE_', LPAD(id - 193000000001000000, 3, '0'))) <> 110 - OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND fee_code = 'FBA_HEAD_FREIGHT' - AND category = '头程运费' AND trade_scope = 'INTERNATIONAL' AND fee_type_name = 'FBA头程运费' - AND pickup_payable = 0 AND pickup_receivable = 0 AND warehouse_payable = 0 - AND warehouse_receivable = 1 AND allocation_fee = 0 AND status = 'ACTIVE' AND deleted = 0) <> 1 - OR EXISTS (SELECT 1 FROM fl_inbound_fee_detail d JOIN fl_fee_item f ON f.id = d.fee_item_id + OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND fee_code = 'FBA_HEAD_FREIGHT' + AND category = '头程运费' AND trade_scope = 'INTERNATIONAL' AND fee_type_name = 'FBA头程运费' + AND pickup_payable = 0 AND pickup_receivable = 0 AND warehouse_payable = 0 + AND warehouse_receivable = 1 AND allocation_fee = 0 AND status = 'ACTIVE' AND deleted = 0) <> 1 + OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND fee_code = 'PRIVATE_LAST_MILE_DELIVERY' + AND category = '尾程派送费' AND trade_scope = 'INTERNATIONAL' + AND fee_type_name = '私人仓尾程派送费' AND warehouse_payable = 1 + AND warehouse_receivable = 1 AND status = 'ACTIVE' AND deleted = 0) <> 1 + OR EXISTS (SELECT 1 FROM fl_inbound_fee_detail d JOIN fl_fee_item f ON f.id = d.fee_item_id WHERE d.tenant_id = 0 OR f.tenant_id <> 0) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期迁移失败:费用项目未完成平台公共作用域或明细跨客户引用校验'; END IF; diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index df24f0f7b..240b9e4e1 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -184,7 +184,7 @@ paths: responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/InboundFeeDetailListApiResponse'}}}}, '403': {description: 无权限}} post: summary: 新增 MANUAL 其他费用 - description: 只允许创建 MANUAL 来源;FBA_PRICING 由系统从 fl_inbound_fee 派生。 + description: 只允许创建 MANUAL 来源;FBA_PRICING 由系统从 fl_inbound_fee 派生。费用项目 PRIVATE_LAST_MILE_DELIVERY 是独立的私人仓尾程派送费,仅 destinationType=PRIVATE 的入库单可创建;服务端按稳定 feeCode 校验,不按中文名称判断。 parameters: [{$ref: '#/components/parameters/AccountId'}, {$ref: '#/components/parameters/InboundId'}] requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/InboundFeeDetailSaveRequest'}}}} responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/InboundFeeDetailApiResponse'}}}}, '403': {description: 无权限}} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FeeItemMigrationSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FeeItemMigrationSqlContractTest.java index 38a44e5ce..144a900f1 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FeeItemMigrationSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FeeItemMigrationSqlContractTest.java @@ -24,6 +24,8 @@ class FeeItemMigrationSqlContractTest { .contains("matched_seed.trade_scope = f.trade_scope") .contains("193000000001000111, 0, 'FBA_HEAD_FREIGHT'") .contains("fee_code = 'FBA_HEAD_FREIGHT'") + .contains("193000000001000112, 0, 'PRIVATE_LAST_MILE_DELIVERY'") + .contains("fee_code = 'PRIVATE_LAST_MILE_DELIVERY'") .contains("warehouse_receivable = 1") .doesNotContain("LEFT JOIN tmp_fee_item_seed by_code") .doesNotContain("LEFT JOIN tmp_fee_item_seed by_semantic") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java index 6588c9a40..a0fdc7b00 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java @@ -337,6 +337,29 @@ class FeeManagementServiceTest { .hasMessageContaining("仓储应收/应付"); } + @Test + @DisplayName("私人仓尾程派送费只允许PRIVATE目的地") + void restrictsPrivateLastMileDeliveryFeeByDestinationType() { + InboundOrderEntity fbaInbound = inbound(); + fbaInbound.setDestinationType("FBA"); + FeeItemEntity privateDelivery = item(true, true); + privateDelivery.setFeeCode("PRIVATE_LAST_MILE_DELIVERY"); + when(repository.findInbound(10001L, 21L)).thenReturn(fbaInbound); + when(repository.findItem(11L)).thenReturn(privateDelivery); + + assertThatThrownBy(() -> details.create(10001L, 21L, 9L, + detailRequest("RECEIVABLE"))) + .isInstanceOf(BizException.class) + .hasMessageContaining("目的地类型为PRIVATE"); + verify(repository, never()).insertDetail(any()); + + fbaInbound.setDestinationType("PRIVATE"); + doAnswer(invocation -> { ((InboundFeeDetailEntity) invocation.getArgument(0)).setId(32L); return null; }) + .when(repository).insertDetail(any()); + assertThat(details.create(10001L, 21L, 9L, detailRequest("RECEIVABLE")).getId()) + .isEqualTo(32L); + } + @Test @DisplayName("审核只更新待审核明细") void approvesPendingDetail() {