From 81c9c92f5d3f8d2251dc9b0446edb2e282b27a69 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 5 Aug 2026 09:30:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-08-04-二期TEST环境增量-MySQL8直接执行.sql | 13 +++++++ ...8-04-二期入库新增表字段-MySQL8一键执行.sql | 13 +++++++ .../PlatformFbaMasterDataController.java | 6 ++-- .../repository/FbaPricingRepository.java | 6 ++-- .../service/FbaMasterDataService.java | 2 +- .../db/phase2-fba-pricing-migration.sql | 14 ++++++++ .../resources/openapi/fba-pricing-v1.yaml | 4 ++- .../controller/FbaPricingContractTest.java | 20 +++++++---- .../FbaPricingRepositoryRuleListTest.java | 36 ++++++++++++++++++- .../InboundWorkflowSqlContractTest.java | 12 +++++++ 10 files changed, 112 insertions(+), 14 deletions(-) diff --git a/deploy/sql/2026-08-04-二期TEST环境增量-MySQL8直接执行.sql b/deploy/sql/2026-08-04-二期TEST环境增量-MySQL8直接执行.sql index 760dd89dc..c8c674ecd 100644 --- a/deploy/sql/2026-08-04-二期TEST环境增量-MySQL8直接执行.sql +++ b/deploy/sql/2026-08-04-二期TEST环境增量-MySQL8直接执行.sql @@ -863,6 +863,19 @@ CALL phase2_fba_add_column('fl_inbound_fee', 'destination_code', "VARCHAR(32) NO CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_from', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '价格生效开始快照' AFTER destination_code"); CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_to', "DATETIME(3) NULL COMMENT '价格生效结束快照' AFTER rate_effective_from"); CALL phase2_fba_add_column('fl_inbound_idempotency', 'result_resource_id', "BIGINT UNSIGNED NULL COMMENT '成功结果资源ID' AFTER result_json"); +CALL phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id', + "BIGINT UNSIGNED NULL COMMENT '仓储资源所属租户ID' AFTER warehouse_id"); + +UPDATE fl_inbound_pallet_allocation +SET warehouse_resource_tenant_id = tenant_id +WHERE warehouse_resource_tenant_id IS NULL OR warehouse_resource_tenant_id = 0; +SET @resource_tenant_nullable = (SELECT IS_NULLABLE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_inbound_pallet_allocation' + AND COLUMN_NAME = 'warehouse_resource_tenant_id'); +SET @sql = IF(@resource_tenant_nullable = 'YES', + "ALTER TABLE fl_inbound_pallet_allocation MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '仓储资源所属租户ID'", + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; DROP PROCEDURE IF EXISTS phase2_fba_add_column; diff --git a/deploy/sql/2026-08-04-二期入库新增表字段-MySQL8一键执行.sql b/deploy/sql/2026-08-04-二期入库新增表字段-MySQL8一键执行.sql index fec532661..6478d11e8 100644 --- a/deploy/sql/2026-08-04-二期入库新增表字段-MySQL8一键执行.sql +++ b/deploy/sql/2026-08-04-二期入库新增表字段-MySQL8一键执行.sql @@ -858,6 +858,19 @@ CALL phase2_fba_add_column('fl_inbound_fee', 'destination_code', "VARCHAR(32) NO CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_from', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '价格生效开始快照' AFTER destination_code"); CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_to', "DATETIME(3) NULL COMMENT '价格生效结束快照' AFTER rate_effective_from"); CALL phase2_fba_add_column('fl_inbound_idempotency', 'result_resource_id', "BIGINT UNSIGNED NULL COMMENT '成功结果资源ID' AFTER result_json"); +CALL phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id', + "BIGINT UNSIGNED NULL COMMENT '仓储资源所属租户ID' AFTER warehouse_id"); + +UPDATE fl_inbound_pallet_allocation +SET warehouse_resource_tenant_id = tenant_id +WHERE warehouse_resource_tenant_id IS NULL OR warehouse_resource_tenant_id = 0; +SET @resource_tenant_nullable = (SELECT IS_NULLABLE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_inbound_pallet_allocation' + AND COLUMN_NAME = 'warehouse_resource_tenant_id'); +SET @sql = IF(@resource_tenant_nullable = 'YES', + "ALTER TABLE fl_inbound_pallet_allocation MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '仓储资源所属租户ID'", + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; DROP PROCEDURE IF EXISTS phase2_fba_add_column; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFbaMasterDataController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFbaMasterDataController.java index a982e78dc..a5980d05a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFbaMasterDataController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFbaMasterDataController.java @@ -76,10 +76,12 @@ public class PlatformFbaMasterDataController { @GetMapping("/carrier-rates") @PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')") + @Operation(summary = "查询船司 FBA 价格版本", description = "默认查询全部企业;tenantId 可用于筛选目标企业") public ApiResponse> rates( - @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam(required = false) Long tenantId, @RequestParam(required = false) String rateCode) { - return ApiResponse.success(service.listRates(FirstlegScopeGuard.platform(user, tenantId), rateCode)); + return ApiResponse.success(service.listRates(optionalPlatformTenant(user, tenantId), rateCode)); } @GetMapping("/carrier-rates/{id}") diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index e2d9b6238..7b8b604a7 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -21,7 +21,7 @@ import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; -/** FBA 计费仓储;平台客户规则列表可跨企业读取,其他业务操作强制带租户条件。 */ +/** FBA 计费仓储;平台基础资料列表可跨企业读取,其他业务操作强制带租户条件。 */ @Repository public class FbaPricingRepository { private final CustomerChargeRuleMapper rules; @@ -69,9 +69,9 @@ public class FbaPricingRepository { .orderByDesc(CustomerChargeRuleEntity::getRuleVersion)); } - public List listRates(long tenantId, String rateCode) { + public List listRates(Long tenantId, String rateCode) { return rates.selectList(new LambdaQueryWrapper() - .eq(CarrierFbaRateEntity::getTenantId, tenantId) + .eq(tenantId != null, CarrierFbaRateEntity::getTenantId, tenantId) .eq(rateCode != null && !rateCode.isBlank(), CarrierFbaRateEntity::getRateCode, rateCode) .orderByAsc(CarrierFbaRateEntity::getRateCode) .orderByDesc(CarrierFbaRateEntity::getRateVersion)); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaMasterDataService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaMasterDataService.java index 4970cbf3e..2924f4e6c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaMasterDataService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaMasterDataService.java @@ -102,7 +102,7 @@ public class FbaMasterDataService { return repository.listRules(tenantId, code); } - public List listRates(long tenantId, String code) { + public List listRates(Long tenantId, String code) { return repository.listRates(tenantId, code); } diff --git a/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql b/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql index 73fa27b54..f36f3cb49 100644 --- a/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql +++ b/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql @@ -40,6 +40,20 @@ CALL phase2_fba_add_column('fl_inbound_fee', 'destination_code', "VARCHAR(32) NO CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_from', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '价格生效开始快照' AFTER destination_code"); CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_to', "DATETIME(3) NULL COMMENT '价格生效结束快照' AFTER rate_effective_from"); CALL phase2_fba_add_column('fl_inbound_idempotency', 'result_resource_id', "BIGINT UNSIGNED NULL COMMENT '成功结果资源ID' AFTER result_json"); +CALL phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id', + "BIGINT UNSIGNED NULL COMMENT '仓储资源所属租户ID' AFTER warehouse_id"); + +-- 旧分配记录创建于共享仓储资源租户拆分前,按原业务租户回填后再收口非空约束。 +UPDATE fl_inbound_pallet_allocation +SET warehouse_resource_tenant_id = tenant_id +WHERE warehouse_resource_tenant_id IS NULL OR warehouse_resource_tenant_id = 0; +SET @resource_tenant_nullable = (SELECT IS_NULLABLE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_inbound_pallet_allocation' + AND COLUMN_NAME = 'warehouse_resource_tenant_id'); +SET @sql = IF(@resource_tenant_nullable = 'YES', + "ALTER TABLE fl_inbound_pallet_allocation MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '仓储资源所属租户ID'", + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; DROP PROCEDURE IF EXISTS phase2_fba_add_column; diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index 1ab2f4ca6..60af9cf57 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -34,7 +34,9 @@ paths: get: summary: 查询船司 FBA 价格版本 description: 需要 firstleg:fba-pricing:rate:view 权限 - parameters: [{$ref: '#/components/parameters/TenantId'}] + parameters: + - {$ref: '#/components/parameters/OptionalTenantId'} + - {name: rateCode, in: query, schema: {type: string, maxLength: 50}} responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/fba-pricing/carrier-rates/versions: post: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java index efc2cadfb..cf912bf50 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java @@ -13,13 +13,15 @@ import static org.assertj.core.api.Assertions.assertThat; @DisplayName("FBA计费OpenAPI契约") class FbaPricingContractTest { @Test - @DisplayName("只有客户规则列表允许省略企业参数") - void onlyCustomerRuleListAllowsOptionalTenant() { - Method rules = method("rules"); - assertThat(rules.getParameterTypes()[1]).isEqualTo(Long.class); - assertThat(rules.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse(); + @DisplayName("只有基础资料列表允许省略企业参数") + void onlyMasterDataListsAllowOptionalTenant() { + for (String listMethod : new String[] {"rules", "rates"}) { + Method method = method(listMethod); + assertThat(method.getParameterTypes()[1]).isEqualTo(Long.class); + assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse(); + } - for (String scopedMethod : new String[] {"rule", "createRule", "retireRule", "rates", + for (String scopedMethod : new String[] {"rule", "createRule", "retireRule", "rate", "createRate", "retireRate"}) { Method method = method(scopedMethod); assertThat(method.getParameterTypes()[1]).isEqualTo(long.class); @@ -56,6 +58,12 @@ class FbaPricingContractTest { .doesNotContain("name: customerName") .doesNotContain("name: chargeMode") .doesNotContain("name: status"); + String carrierRates = openapi.substring( + openapi.indexOf("/api/platform/v1/fba-pricing/carrier-rates:"), + openapi.indexOf("/api/platform/v1/fba-pricing/carrier-rates/versions:")); + assertThat(carrierRates) + .contains("OptionalTenantId") + .contains("name: rateCode"); assertThat(openapi) .contains("OptionalTenantId: {name: tenantId, in: query, required: false") .contains("schema: {type: string, pattern: '^[1-9][0-9]*$'}") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java index de5cf90b1..5766d4fac 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java @@ -3,6 +3,7 @@ package com.baiyue.firstleg.repository; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baiyue.firstleg.entity.CustomerChargeRuleEntity; +import com.baiyue.firstleg.entity.CarrierFbaRateEntity; import com.baiyue.firstleg.mapper.CarrierFbaRateMapper; import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper; import com.baiyue.firstleg.mapper.InboundFeeItemMapper; @@ -29,6 +30,8 @@ class FbaPricingRepositoryRuleListTest { static void initializeEntityMetadata() { TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"), CustomerChargeRuleEntity.class); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"), + CarrierFbaRateEntity.class); } @Test @@ -41,6 +44,17 @@ class FbaPricingRepositoryRuleListTest { assertThat(queryValues(10001L, "CR-1")).containsExactlyInAnyOrder(10001L, "CR-1"); } + @Test + void platformRateListOmitsTenantConditionWhenTenantIsAbsent() { + assertThat(rateQueryValues(null, null)).isEmpty(); + } + + @Test + void platformRateListKeepsTenantAndRateCodeFiltersWhenProvided() { + assertThat(rateQueryValues(10001L, "RATE-1")) + .containsExactlyInAnyOrder(10001L, "RATE-1"); + } + @Test void internalVersionCalculationKeepsTenantAndRuleCodeFilters() { CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class); @@ -76,8 +90,28 @@ class FbaPricingRepositoryRuleListTest { return wrapper.getValue().getParamNameValuePairs().values(); } + private static Collection rateQueryValues(Long tenantId, String rateCode) { + CarrierFbaRateMapper rates = mock(CarrierFbaRateMapper.class); + when(rates.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of()); + FbaPricingRepository repository = repository(mock(CustomerChargeRuleMapper.class), rates); + + repository.listRates(tenantId, rateCode); + + @SuppressWarnings("unchecked") + ArgumentCaptor> wrapper = + ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(rates).selectList(wrapper.capture()); + wrapper.getValue().getSqlSegment(); + return wrapper.getValue().getParamNameValuePairs().values(); + } + private static FbaPricingRepository repository(CustomerChargeRuleMapper rules) { - return new FbaPricingRepository(rules, mock(CarrierFbaRateMapper.class), + return repository(rules, mock(CarrierFbaRateMapper.class)); + } + + private static FbaPricingRepository repository( + CustomerChargeRuleMapper rules, CarrierFbaRateMapper rates) { + return new FbaPricingRepository(rules, rates, mock(InboundPricingMapper.class), mock(InboundFeeMapper.class), mock(InboundFeeItemMapper.class), mock(InboundPricingIdempotencyMapper.class), mock(InboundOutboxMapper.class)); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java index 757deeb90..71783181b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java @@ -26,4 +26,16 @@ class InboundWorkflowSqlContractTest { .contains("information_schema.statistics") .contains("idx_review_exception_release"); } + + @Test + @DisplayName("存量托盘分配表会补齐仓储资源租户") + void shouldMigrateWarehouseResourceTenantForExistingAllocationTable() throws Exception { + String migration = new ClassPathResource("db/phase2-fba-pricing-migration.sql") + .getContentAsString(StandardCharsets.UTF_8); + + assertThat(migration) + .contains("phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id'") + .contains("SET warehouse_resource_tenant_id = tenant_id") + .contains("MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL"); + } }