From da9aa9515e28090d4ceacef476a9bec0aa625b45 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 4 Aug 2026 11:14:42 +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 --- config/nacos/cn/order-service.yml | 3 + config/nacos/cn/warehouse-service.yml | 4 ++ config/nacos/local/order-service.yml | 3 + config/nacos/local/warehouse-service.yml | 4 ++ .../baiyue/order/client/WarehouseClient.java | 3 +- .../service/OrderApplicationService.java | 9 ++- .../order/service/OrderMasterDataGuard.java | 44 ++++++++++---- .../OrderApplicationServiceStateTest.java | 2 +- .../service/OrderMasterDataGuardTest.java | 60 ++++++++++++++++++- .../MerchantWarehouseController.java | 16 +++-- .../warehouse/dto/WarehouseRequests.java | 6 +- .../repository/WarehouseRepository.java | 3 +- .../db/phase1-warehouse-schema-migration.sql | 3 + warehouse/src/main/resources/db/warehouse.sql | 3 +- 14 files changed, 135 insertions(+), 28 deletions(-) diff --git a/config/nacos/cn/order-service.yml b/config/nacos/cn/order-service.yml index ffcc1335b..b21c16cff 100644 --- a/config/nacos/cn/order-service.yml +++ b/config/nacos/cn/order-service.yml @@ -8,3 +8,6 @@ spring: baiyue.clients: # 容器环境默认通过 Nacos 的 warehouse-service 实例调用;仅端口回归时显式设置直连 URL。 warehouse.url: ${WAREHOUSE_SERVICE_URL:} +baiyue: + warehouse: + public-tenant-id: ${DEFAULT_TENANT_ID:10001} diff --git a/config/nacos/cn/warehouse-service.yml b/config/nacos/cn/warehouse-service.yml index a05d57b33..d01920f6a 100644 --- a/config/nacos/cn/warehouse-service.yml +++ b/config/nacos/cn/warehouse-service.yml @@ -4,3 +4,7 @@ spring: username: ${WAREHOUSE_DB_USERNAME} password: ${WAREHOUSE_DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + +baiyue: + warehouse: + public-tenant-id: ${DEFAULT_TENANT_ID:10001} diff --git a/config/nacos/local/order-service.yml b/config/nacos/local/order-service.yml index f46a32771..c14d89074 100644 --- a/config/nacos/local/order-service.yml +++ b/config/nacos/local/order-service.yml @@ -5,3 +5,6 @@ spring.datasource: password: ${ORDER_DB_PASSWORD:123456} baiyue.clients: warehouse.url: ${WAREHOUSE_SERVICE_URL:} +baiyue: + warehouse: + public-tenant-id: ${DEFAULT_TENANT_ID:10001} diff --git a/config/nacos/local/warehouse-service.yml b/config/nacos/local/warehouse-service.yml index b0e8a969c..e6fc0338b 100644 --- a/config/nacos/local/warehouse-service.yml +++ b/config/nacos/local/warehouse-service.yml @@ -3,3 +3,7 @@ spring.datasource: url: ${WAREHOUSE_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_warehouse?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true} username: ${WAREHOUSE_DB_USERNAME:root} password: ${WAREHOUSE_DB_PASSWORD:123456} + +baiyue: + warehouse: + public-tenant-id: ${DEFAULT_TENANT_ID:10001} diff --git a/order/src/main/java/com/baiyue/order/client/WarehouseClient.java b/order/src/main/java/com/baiyue/order/client/WarehouseClient.java index a98ba07e7..8cf47e700 100644 --- a/order/src/main/java/com/baiyue/order/client/WarehouseClient.java +++ b/order/src/main/java/com/baiyue/order/client/WarehouseClient.java @@ -12,7 +12,8 @@ public interface WarehouseClient { ApiResponse active(@RequestHeader("X-Internal-Service-Token") String token, @PathVariable("tenantId") long tenantId, @PathVariable("id") long id); record Detail(Warehouse warehouse, List
addresses, List capabilities) { } - record Warehouse(Long id, Long tenantId, String warehouseName, String status) { } + record Warehouse(Long id, Long tenantId, String warehouseCode, String warehouseName, + String warehouseType, String countryCode, String status) { } record Address(String countryCode, String stateProvince, String city, String district, String postalCode, String addressLine1, String addressLine2) { } record Capability(String capabilityCode, Boolean enabled, LocalTime cutoffTime) { } diff --git a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java index 3a4228183..423f6152b 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -81,7 +81,7 @@ public class OrderApplicationService { if (target == OrderStatus.SUBMITTED) { validatePersistedPrototypeFields(tenantId, order); masterDataGuard.validatePersisted(tenantId, order.getCustomerId(), order.getReceivingWarehouseId(), - order.getDestinationWarehouseId()); + order.getDestinationWarehouseId(), order.getDestinationType()); } if ((target == OrderStatus.REJECTED || target == OrderStatus.CANCELLED) && !StringUtils.hasText(reason)) { throw new BizException("Reason is required"); @@ -274,7 +274,8 @@ public class OrderApplicationService { private void apply(OrderEntity order, OrderRequests.SaveRequest request, OrderMasterDataGuard.Snapshots snapshots) { order.setCustomerId(snapshots.partner().id()); order.setCustomerName(snapshots.partner().partnerName()); order.setTransportMode(request.transportMode()); order.setTradeTerm(request.tradeTerm()); - order.setDestinationCountryCode(request.destinationCountryCode()); + order.setDestinationCountryCode(snapshots.destination() == null ? request.destinationCountryCode() + : snapshots.destination().warehouse().countryCode()); order.setDestinationWarehouseId(request.destinationWarehouseId()); order.setDestinationWarehouseName(snapshots.destination() == null ? request.destinationWarehouseName() : snapshots.destination().warehouse().warehouseName()); @@ -286,7 +287,9 @@ public class OrderApplicationService { order.setChannelCode(request.channelCode()); order.setChannelName(request.channelName()); order.setTaxMode(request.taxMode()); order.setContainerMode(request.containerMode()); order.setCustomsInspection(request.customsInspection()); - order.setDestinationType(request.destinationType()); order.setAmazonWarehouseCode(request.amazonWarehouseCode()); + order.setDestinationType(request.destinationType()); + order.setAmazonWarehouseCode("FBA".equals(request.destinationType()) && snapshots.destination() != null + ? snapshots.destination().warehouse().warehouseCode() : request.amazonWarehouseCode()); order.setShipmentId(request.shipmentId()); order.setReferenceId(request.referenceId()); } private String formatAddress(com.baiyue.order.client.WarehouseClient.Detail detail) { diff --git a/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java b/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java index 202801cd8..01a6fe9d4 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java +++ b/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java @@ -21,6 +21,8 @@ public class OrderMasterDataGuard { private final WarehouseClient warehouses; private final PartnerClient partners; private final String token; + @Value("${baiyue.warehouse.public-tenant-id:10001}") + private long publicTenantId = 10001L; public OrderMasterDataGuard(WarehouseClient warehouses, PartnerClient partners, @Value("${baiyue.security.internal-service-token}") String token) { this.warehouses=warehouses; this.partners=partners; this.token=token; @@ -29,15 +31,24 @@ public class OrderMasterDataGuard { if (request.customerId() == null) throw new BizException(ResultCode.VALIDATION_FAILED, "必须选择往来单位"); try { var partner=loadPartner(tenantId,request.customerId()); - var receiving=loadWarehouse(tenantId,request.receivingWarehouseId(),"收货仓库"); + var receiving=loadWarehouse(request.receivingWarehouseId(),"收货仓库"); + requireWarehouseType(receiving, "DOMESTIC", "收货仓库"); if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull)) throw new BizException(ResultCode.VALIDATION_FAILED,"收货仓库未配置地址"); if (receiving.capabilities()==null || receiving.capabilities().stream().noneMatch(c -> c != null && "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled()))) throw new BizException(ResultCode.VALIDATION_FAILED,"仓库未启用收货能力"); WarehouseClient.Detail destination=null; - if (request.destinationWarehouseId()!=null) { - destination=loadWarehouse(tenantId,request.destinationWarehouseId(),"目的仓库"); + if ("PRIVATE".equals(request.destinationType())) { + if (request.destinationWarehouseId()!=null) { + throw new BizException(ResultCode.VALIDATION_FAILED,"私人地址不能引用公共仓库"); + } + } else { + if (request.destinationWarehouseId()==null) { + throw new BizException(ResultCode.VALIDATION_FAILED,"请选择平台维护的目的仓库"); + } + destination=loadWarehouse(request.destinationWarehouseId(),"目的仓库"); + requireWarehouseType(destination, request.destinationType(), "目的仓库"); } return new Snapshots(partner,receiving,destination); } catch (FeignException exception) { @@ -48,13 +59,21 @@ public class OrderMasterDataGuard { throw downstreamFailure(exception); } } - public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId, Long destinationWarehouseId) { + public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId, + Long destinationWarehouseId, String destinationType) { try { if (partnerId==null) throw new BizException(ResultCode.NOT_FOUND,"往来单位已停用或不可用"); loadPartner(tenantId,partnerId); if (receivingWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"收货仓库已停用或不可用"); - loadWarehouse(tenantId,receivingWarehouseId,"收货仓库"); - if (destinationWarehouseId!=null) loadWarehouse(tenantId,destinationWarehouseId,"目的仓库"); + WarehouseClient.Detail receiving=loadWarehouse(receivingWarehouseId,"收货仓库"); + requireWarehouseType(receiving,"DOMESTIC","收货仓库"); + if ("PRIVATE".equals(destinationType)) { + if (destinationWarehouseId!=null) throw new BizException(ResultCode.VALIDATION_FAILED,"私人地址不能引用公共仓库"); + } else { + if (destinationWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"目的仓库已停用或不可用"); + WarehouseClient.Detail destination=loadWarehouse(destinationWarehouseId,"目的仓库"); + requireWarehouseType(destination,destinationType,"目的仓库"); + } } catch (FeignException exception) { throw downstreamFailure(exception); } catch (BizException exception) { @@ -82,18 +101,18 @@ public class OrderMasterDataGuard { if (!StringUtils.hasText(value.partnerName())) throw unavailable("往来单位"); return value; } - private WarehouseClient.Detail loadWarehouse(long tenantId,long id,String name) { + private WarehouseClient.Detail loadWarehouse(long id,String name) { ApiResponse response; try { - response=warehouses.active(token,tenantId,id); + response=warehouses.active(token,publicTenantId,id); } catch (FeignException.NotFound exception) { throw new BizException(ResultCode.NOT_FOUND,"\u6536\u8d27\u4ed3\u5e93\u4e0d\u5b58\u5728\u6216\u5df2\u505c\u7528"); } requireSuccessful(response,name); if (response.data()==null || response.data().warehouse()==null) throw unavailable(name); var value=response.data(); - if (!Long.valueOf(tenantId).equals(value.warehouse().tenantId())) - throw new BizException(ResultCode.FORBIDDEN,name+"不属于当前企业"); + if (!Long.valueOf(publicTenantId).equals(value.warehouse().tenantId())) + throw new BizException(ResultCode.FORBIDDEN,name+"不属于公共仓库配置"); if (!"ACTIVE".equals(value.warehouse().status())) throw new BizException(ResultCode.CONFLICT,name+"已停用"); if (!Long.valueOf(id).equals(value.warehouse().id()) || !StringUtils.hasText(value.warehouse().warehouseName())) { @@ -101,6 +120,11 @@ public class OrderMasterDataGuard { } return value; } + private void requireWarehouseType(WarehouseClient.Detail detail, String expected, String name) { + if (detail == null || detail.warehouse() == null || !expected.equals(detail.warehouse().warehouseType())) { + throw new BizException(ResultCode.VALIDATION_FAILED,name+"类型必须为 "+expected); + } + } private BizException unavailable(String name) { return new BizException(ResultCode.SERVICE_UNAVAILABLE,name+"服务暂时不可用"); } diff --git a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java index 92b2260ee..3a940e572 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java @@ -123,7 +123,7 @@ class OrderApplicationServiceStateTest { private OrderMasterDataGuard.Snapshots snapshots() { var partner = new PartnerClient.Partner(7L, 10001L, "客户", "ACTIVE", "SHIPPER", List.of("SHIPPER")); - var warehouse = new WarehouseClient.Warehouse(8L, 10001L, "收货仓", "ACTIVE"); + var warehouse = new WarehouseClient.Warehouse(8L, 10001L, "CN-SZX-01", "收货仓", "DOMESTIC", "CN", "ACTIVE"); var address = new WarehouseClient.Address("CN", "广东", "深圳", "南山", "518000", "测试路1号", null); var capability = new WarehouseClient.Capability("RECEIVING", true, LocalTime.of(16, 0)); var receiving = new WarehouseClient.Detail(warehouse, List.of(address), List.of(capability)); diff --git a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java index 1d65c5bac..89a72cc32 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java @@ -21,13 +21,15 @@ import static org.mockito.Mockito.*; @DisplayName("订单主数据校验") class OrderMasterDataGuardTest { + private static final long PUBLIC_TENANT_ID = 10001L; + @Test void validMasterDataReturnsTrustedServerSnapshot() { WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER")); var address=new WarehouseClient.Address("CN","广东省","深圳市","宝安区","518000","示例路1号",null); var capability=new WarehouseClient.Capability("RECEIVING",true,java.time.LocalTime.of(16,0)); - var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"),List.of(address),List.of(capability)); + var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),List.of(address),List.of(capability)); when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner)); when(warehouses.active("token",10001,8)).thenReturn(ApiResponse.success(wh)); var result=new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L)); @@ -58,7 +60,7 @@ class OrderMasterDataGuardTest { void masterDataResponseWithNullElementReturnsControlledBusinessError() { WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER")); - var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"), + var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"), Collections.singletonList(null), Collections.singletonList(null)); when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner)); when(warehouses.active("token",10001,8)).thenReturn(ApiResponse.success(wh)); @@ -153,6 +155,47 @@ class OrderMasterDataGuardTest { .isEqualTo(ResultCode.VALIDATION_FAILED); verifyNoInteractions(warehouses); } + @Test + void customerTenantCanUseDomesticWarehouseFromPublicTenant() { + WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); + when(partners.active("token",20002,7)).thenReturn(ApiResponse.success( + new PartnerClient.Partner(7L,20002L,"客户","ACTIVE","SHIPPER",List.of("SHIPPER")))); + when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success( + warehouse(8L,"DOMESTIC",true))); + + new OrderMasterDataGuard(warehouses,partners,"token").validate(20002,request(7L,8L)); + + verify(warehouses).active("token",PUBLIC_TENANT_ID,8); + } + @Test + void fbaDestinationMustReferenceMatchingPublicWarehouse() { + WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); + when(partners.active("token",20002,7)).thenReturn(ApiResponse.success( + new PartnerClient.Partner(7L,20002L,"客户","ACTIVE","SHIPPER",List.of("SHIPPER")))); + when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success( + warehouse(8L,"DOMESTIC",true))); + when(warehouses.active("token",PUBLIC_TENANT_ID,9)).thenReturn(ApiResponse.success( + warehouse(9L,"OVERSEAS",false))); + + assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token") + .validate(20002,request(7L,8L,"FBA",9L))) + .isInstanceOf(BizException.class) + .hasMessageContaining("目的仓库类型必须为 FBA"); + } + @Test + void privateDestinationCannotReferencePublicWarehouse() { + WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); + when(partners.active("token",20002,7)).thenReturn(ApiResponse.success( + new PartnerClient.Partner(7L,20002L,"客户","ACTIVE","SHIPPER",List.of("SHIPPER")))); + when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success( + warehouse(8L,"DOMESTIC",true))); + + assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token") + .validate(20002,request(7L,8L,"PRIVATE",9L))) + .isInstanceOf(BizException.class) + .hasMessageContaining("私人地址不能引用公共仓库"); + verify(warehouses,never()).active("token",PUBLIC_TENANT_ID,9); + } private FeignException feign(int status) { Request request = Request.create(Request.HttpMethod.GET, "http://warehouse-service/internal/test", Map.of(), null, StandardCharsets.UTF_8, null); @@ -161,9 +204,20 @@ class OrderMasterDataGuardTest { return FeignException.errorStatus("masterData", response); } private OrderRequests.SaveRequest request(Long partner,Long warehouse) { + return request(partner,warehouse,"PRIVATE",null); + } + private OrderRequests.SaveRequest request(Long partner,Long warehouse,String destinationType,Long destinationWarehouseId) { var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null); var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE, null,null,null,null,null,false,null,null,null,null,null,null,null,null,null,null,null,null,null); - return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",null,"不可信目的仓",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道","BUY_DECLARATION","LCL",false,"OVERSEAS",null,null,null); + return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道","BUY_DECLARATION","LCL",false,destinationType,null,null,null); + } + private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) { + var value=new WarehouseClient.Warehouse(id,PUBLIC_TENANT_ID,"WH-"+id,"仓库"+id,type,"CN","ACTIVE"); + var address=new WarehouseClient.Address("CN","广东省","深圳市",null,null,"示例地址",null); + var capabilities=receiving + ? List.of(new WarehouseClient.Capability("RECEIVING",true,null)) + : List.of(); + return new WarehouseClient.Detail(value,List.of(address),capabilities); } } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantWarehouseController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantWarehouseController.java index f70a87f2d..7a2cac41d 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantWarehouseController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantWarehouseController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -20,11 +21,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** - * 商户端仓库查询接口,仅允许在认证令牌所属租户范围内读取仓库列表和详情。 + * 商户端公共仓库查询接口。认证用户共享平台默认租户维护的仓库配置。 */ @Tag( name = "Merchant warehouses / 商户仓库", - description = "Merchant read APIs restricted to the tenant in the authenticated JWT. / 商户端只读接口,租户范围取自认证 JWT。" + description = "Merchant read APIs for shared public warehouses. / 商户端只读公共仓库接口。" ) @RestController @RequestMapping("/api/merchant/v1/warehouses") @@ -32,9 +33,12 @@ public class MerchantWarehouseController { @Resource private WarehouseApplicationService service; + @Value("${baiyue.warehouse.public-tenant-id:10001}") + private long publicTenantId; + @Operation( summary = "List available warehouses / 查询可用仓库", - description = "Tenant comes only from the authenticated JWT; clients cannot override it. / 租户仅从认证 JWT 获取,客户端不可覆盖。" + description = "Returns active public warehouses maintained under the configured public tenant. / 返回平台默认租户维护的可用公共仓库。" ) @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"), @@ -44,12 +48,12 @@ public class MerchantWarehouseController { public ApiResponse> page( @AuthenticationPrincipal AuthenticatedUser user, @Valid WarehouseRequests.QueryRequest query) { - return ApiResponse.success(service.merchantPage(user.tenantId(), query)); + return ApiResponse.success(service.merchantPage(publicTenantId, query)); } @Operation( summary = "Get warehouse details / 查询仓库详情", - description = "Returns warehouse details only within the authenticated merchant tenant. / 仅返回当前认证商户租户内的仓库详情。" + description = "Returns a public warehouse maintained under the configured public tenant. / 返回平台默认租户维护的公共仓库详情。" ) @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"), @@ -60,6 +64,6 @@ public class MerchantWarehouseController { @AuthenticationPrincipal AuthenticatedUser user, @Parameter(description = "Warehouse ID / 仓库 ID", example = "190000000000000001", required = true) @PathVariable long id) { - return ApiResponse.success(service.activeDetail(user.tenantId(), id)); + return ApiResponse.success(service.activeDetail(publicTenantId, id)); } } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseRequests.java b/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseRequests.java index 079625e5b..07b07f860 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseRequests.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseRequests.java @@ -29,8 +29,8 @@ public final class WarehouseRequests { @NotBlank @Size(max = 50) String warehouseCode, @Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓", maxLength = 120, requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @Size(max = 120) String warehouseName, - @Schema(description = "Warehouse type / 仓库类型", example = "OVERSEAS", requiredMode = Schema.RequiredMode.REQUIRED) - @NotBlank String warehouseType, + @Schema(description = "Warehouse type / 仓库类型", example = "OVERSEAS", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS"}, requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank @Pattern(regexp = "DOMESTIC|FBA|OVERSEAS") String warehouseType, @Schema(description = "ISO 3166-1 alpha-2 country code / ISO 两位国家代码", example = "US", pattern = "[A-Z]{2}", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @Pattern(regexp = "[A-Z]{2}") String countryCode, @Schema(description = "IANA timezone / IANA 时区", example = "America/Los_Angeles", requiredMode = Schema.RequiredMode.REQUIRED) @@ -102,6 +102,8 @@ public final class WarehouseRequests { String countryCode, @Schema(description = "Enabled capability code filter / 已启用能力编码筛选", example = "STORAGE") String capabilityCode, + @Schema(description = "Warehouse type filter / 仓库类型筛选", example = "DOMESTIC", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS"}) + @Pattern(regexp = "DOMESTIC|FBA|OVERSEAS") String warehouseType, @Schema(description = "Status filter / 状态筛选", example = "ACTIVE", allowableValues = {"ACTIVE", "INACTIVE"}) String status ) { diff --git a/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java b/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java index cbda164f4..27cb86cb0 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java @@ -64,6 +64,7 @@ public class WarehouseRepository { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(WarehouseEntity::getTenantId, tenantId) .eq(StringUtils.hasText(query.status()), WarehouseEntity::getStatus, query.status()) + .eq(StringUtils.hasText(query.warehouseType()), WarehouseEntity::getWarehouseType, query.warehouseType()) .eq(StringUtils.hasText(query.countryCode()), WarehouseEntity::getCountryCode, query.countryCode()) .orderByDesc(WarehouseEntity::getCreatedAt); @@ -87,7 +88,7 @@ public class WarehouseRepository { public PageResult activePage(long tenantId, WarehouseRequests.QueryRequest query) { return page(tenantId, new WarehouseRequests.QueryRequest(query.page(), query.size(), - query.countryCode(), query.capabilityCode(), "ACTIVE")); + query.countryCode(), query.capabilityCode(), query.warehouseType(), "ACTIVE")); } public List findAddresses(long tenantId, long warehouseId) { diff --git a/warehouse/src/main/resources/db/phase1-warehouse-schema-migration.sql b/warehouse/src/main/resources/db/phase1-warehouse-schema-migration.sql index 3b32f1fc3..bee2faf1f 100644 --- a/warehouse/src/main/resources/db/phase1-warehouse-schema-migration.sql +++ b/warehouse/src/main/resources/db/phase1-warehouse-schema-migration.sql @@ -126,6 +126,9 @@ SET @schema_name = DATABASE(); SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = @schema_name AND table_name = 'wh_warehouse' AND index_name = 'idx_wh_tenant_status') = 0, 'ALTER TABLE wh_warehouse ADD KEY idx_wh_tenant_status (tenant_id, status, deleted)', 'SELECT 1'); PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement; +SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = @schema_name AND table_name = 'wh_warehouse' AND index_name = 'idx_wh_tenant_type_status') = 0, + 'ALTER TABLE wh_warehouse ADD KEY idx_wh_tenant_type_status (tenant_id, warehouse_type, status, deleted)', 'SELECT 1'); +PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement; SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = @schema_name AND table_name = 'wh_warehouse_address' AND index_name = 'idx_address_warehouse') = 0, 'ALTER TABLE wh_warehouse_address ADD KEY idx_address_warehouse (tenant_id, warehouse_id, deleted)', 'SELECT 1'); PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement; diff --git a/warehouse/src/main/resources/db/warehouse.sql b/warehouse/src/main/resources/db/warehouse.sql index b157f4f75..074e90d0b 100644 --- a/warehouse/src/main/resources/db/warehouse.sql +++ b/warehouse/src/main/resources/db/warehouse.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS wh_warehouse ( tenant_id BIGINT UNSIGNED NOT NULL COMMENT '仓库所属租户ID', warehouse_code VARCHAR(50) NOT NULL COMMENT '租户内唯一仓库编码', warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称', - warehouse_type VARCHAR(32) NOT NULL DEFAULT 'OVERSEAS' COMMENT '仓库类型', + warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC、FBA、OVERSEAS', country_code CHAR(2) NOT NULL COMMENT '所在国家二字码', timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地IANA时区', contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人', @@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS wh_warehouse ( PRIMARY KEY (id), UNIQUE KEY uk_wh_tenant_code (tenant_id, warehouse_code, deleted), KEY idx_wh_tenant_status (tenant_id, status, deleted), + KEY idx_wh_tenant_type_status (tenant_id, warehouse_type, status, deleted), KEY idx_wh_country (tenant_id, country_code, deleted) ) ENGINE=InnoDB COMMENT='仓库主数据表:以租户为边界维护仓库编码、名称、国家、时区和启停状态';