From 83049ed9e5c93759f701b019cfc7c83c9ebcb41f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 10 Oct 2026 11:52:29 +0800 Subject: [PATCH] feat(warehouse): provide bounded internal historical metadata batch --- warehouse/pom.xml | 5 +++ .../InternalWarehouseController.java | 10 ++++- .../warehouse/dto/WarehouseMetadataBatch.java | 13 ++++++ .../warehouse/entity/WarehouseEntity.java | 2 +- .../repository/WarehouseRepository.java | 8 ++++ .../service/WarehouseApplicationService.java | 20 +++++++++ .../main/resources/openapi/warehouse-v1.yaml | 43 +++++++++++++++++++ .../InternalWarehouseControllerTest.java | 16 +++++++ .../WarehouseMetadataPersistenceTest.java | 29 +++++++++++++ .../service/WarehouseMetadataBatchTest.java | 33 ++++++++++++++ 10 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseMetadataBatch.java create mode 100644 warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseMetadataPersistenceTest.java create mode 100644 warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseMetadataBatchTest.java diff --git a/warehouse/pom.xml b/warehouse/pom.xml index 1813019e2..4b5d0438b 100644 --- a/warehouse/pom.xml +++ b/warehouse/pom.xml @@ -12,6 +12,11 @@ warehouse-service + + com.h2database + h2 + test + org.apache.poi poi-ooxml diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java index 0b1f3b1e5..d03baaf9d 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java @@ -28,6 +28,14 @@ public class InternalWarehouseController { private String internalToken; private byte[] expected; + @PostMapping("/platform-metadata/batch") + public ApiResponse platformMetadata( + @RequestHeader(value="X-Internal-Service-Token",required=false) String token, + @jakarta.validation.Valid @RequestBody com.baiyue.warehouse.dto.WarehouseMetadataBatch.Request request) { + verifyToken(token); + return ApiResponse.success(service.platformMetadata(request.warehouseIds())); + } + @PostConstruct void initializeToken() { this.expected = internalToken.getBytes(StandardCharsets.UTF_8); @@ -81,7 +89,7 @@ public class InternalWarehouseController { private void verifyToken(String token) { byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8); - if (!MessageDigest.isEqual(expected, supplied)) { + if (expected == null || expected.length == 0 || !MessageDigest.isEqual(expected, supplied)) { throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效"); } } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseMetadataBatch.java b/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseMetadataBatch.java new file mode 100644 index 000000000..9d3c29460 --- /dev/null +++ b/warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseMetadataBatch.java @@ -0,0 +1,13 @@ +package com.baiyue.warehouse.dto; + +import jakarta.validation.constraints.*; +import java.util.List; + +/** Minimal public warehouse facts for internal historical read projections. */ +public final class WarehouseMetadataBatch { + private WarehouseMetadataBatch() { } + public record Request(@NotEmpty @Size(max=2000) List<@NotNull @Positive Long> warehouseIds) { } + public record Item(Long id,String warehouseCode,String warehouseName,String warehouseType, + String countryCode,String timezone,String status) { } + public record Response(List records,List missingIds) { } +} diff --git a/warehouse/src/main/java/com/baiyue/warehouse/entity/WarehouseEntity.java b/warehouse/src/main/java/com/baiyue/warehouse/entity/WarehouseEntity.java index 358fa93d5..a8c21f678 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/entity/WarehouseEntity.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/entity/WarehouseEntity.java @@ -34,7 +34,7 @@ public class WarehouseEntity extends BaseEntity { private String warehouseType; @Schema(description = "ISO two-letter country code / ISO 两位国家代码", example = "US") private String countryCode; - @Schema(description = "UTC offset / UTC 偏移量", example = "UTC-8") + @Schema(description = "IANA location time zone / 地点时区,未核实为空", example = "America/Los_Angeles") private String timezone; @Schema(description = "Contact name / 联系人", example = "Alex Chen") private String contactName; 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 87ff92e64..a6e98a72e 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java @@ -46,6 +46,14 @@ public class WarehouseRepository { .eq(WarehouseEntity::getId, id)); } + public List platformMetadata(List ids) { + return warehouses.selectList(new LambdaQueryWrapper() + .select(WarehouseEntity::getId,WarehouseEntity::getTenantId,WarehouseEntity::getWarehouseCode, + WarehouseEntity::getWarehouseName,WarehouseEntity::getWarehouseType,WarehouseEntity::getCountryCode, + WarehouseEntity::getTimezone,WarehouseEntity::getStatus) + .eq(WarehouseEntity::getTenantId,0L).in(WarehouseEntity::getId,ids)); + } + public WarehouseEntity findByCode(long tenantId, String code) { return warehouses.selectOne(new LambdaQueryWrapper() .eq(WarehouseEntity::getTenantId, tenantId) diff --git a/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java b/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java index 0ae0602ac..8e57fe959 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java @@ -28,6 +28,26 @@ public class WarehouseApplicationService { @Resource private WarehouseRepository repository; + @Transactional(readOnly=true) + public com.baiyue.warehouse.dto.WarehouseMetadataBatch.Response platformMetadata(List ids) { + if(ids==null || ids.isEmpty() || ids.size()>2000 || ids.stream().anyMatch(id->id==null || id<=0) + || new java.util.HashSet<>(ids).size()!=ids.size()) + throw new BizException(ResultCode.VALIDATION_FAILED,"每次需提供1至2000个不重复的有效仓库标识"); + var byId=new java.util.HashMap(); + for(var row:repository.platformMetadata(ids)) { + if(Long.valueOf(0L).equals(row.getTenantId()) && ids.contains(row.getId())) byId.put(row.getId(),row); + } + var records=new java.util.ArrayList(); + var missing=new java.util.ArrayList(); + for(var id:ids) { + var row=byId.get(id); + if(row==null) missing.add(id); + else records.add(new com.baiyue.warehouse.dto.WarehouseMetadataBatch.Item(row.getId(),row.getWarehouseCode(), + row.getWarehouseName(),row.getWarehouseType(),row.getCountryCode(),row.getTimezone(),row.getStatus())); + } + return new com.baiyue.warehouse.dto.WarehouseMetadataBatch.Response(List.copyOf(records),List.copyOf(missing)); + } + @Transactional @BusinessAudit( diff --git a/warehouse/src/main/resources/openapi/warehouse-v1.yaml b/warehouse/src/main/resources/openapi/warehouse-v1.yaml index 75d263d1b..a8b963737 100644 --- a/warehouse/src/main/resources/openapi/warehouse-v1.yaml +++ b/warehouse/src/main/resources/openapi/warehouse-v1.yaml @@ -3,6 +3,49 @@ info: title: Warehouse catalog and receiving options version: 1.0.0 paths: + /internal/warehouses/platform-metadata/batch: + post: + summary: 内部批量读取公共仓库历史展示资料 + description: 只读;必须验证X-Internal-Service-Token,不经公网Gateway。固定tenant_id=0,包含ACTIVE和INACTIVE,已删除、非公共和不存在的ID统一返回missingIds。不返回联系人、电话、地址或客户资料。原timezone保留原值/空值,不猜测或修复;消费者必须自行核对地点时区和国家。每批最多2000个不重复正数ID,一次SQL查询。 + parameters: + - {name: X-Internal-Service-Token, in: header, required: true, schema: {type: string}} + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [warehouseIds] + properties: + warehouseIds: {type: array, minItems: 1, maxItems: 2000, uniqueItems: true, items: {type: string, pattern: '^[1-9][0-9]*$'}} + responses: + '200': + description: 成功业务响应code为200,按请求顺序返回可读记录及缺失ID + content: + application/json: + schema: + type: object + properties: + code: {type: string} + data: + type: object + required: [records, missingIds] + properties: + missingIds: {type: array, items: {type: string}} + records: + type: array + items: + type: object + properties: + id: {type: string} + warehouseCode: {type: string} + warehouseName: {type: string} + warehouseType: {type: string} + countryCode: {type: string, nullable: true} + timezone: {type: string, nullable: true} + status: {type: string} + '403': {description: 内部服务凭据错误或缺失} + '422': {description: ID为空、非正数、重复或超过2000个} /api/platform/v1/warehouse-admin/overseas-receiving-options: get: summary: 分页查询启用的公共海外收货仓 diff --git a/warehouse/src/test/java/com/baiyue/warehouse/controller/InternalWarehouseControllerTest.java b/warehouse/src/test/java/com/baiyue/warehouse/controller/InternalWarehouseControllerTest.java index 5aa71a2b7..0f566ffe9 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/controller/InternalWarehouseControllerTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/controller/InternalWarehouseControllerTest.java @@ -22,6 +22,22 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class InternalWarehouseControllerTest { + @Test + void metadataRejectsMissingAndIncorrectInternalCredentialBeforeReading() { + var request=new com.baiyue.warehouse.dto.WarehouseMetadataBatch.Request(List.of(1L)); + assertThatThrownBy(()->controller.platformMetadata(null,request)).isInstanceOf(BizException.class); + assertThatThrownBy(()->controller.platformMetadata("wrong",request)).isInstanceOf(BizException.class); + verifyNoInteractions(service); + controller.platformMetadata(INTERNAL_TOKEN,request);verify(service).platformMetadata(List.of(1L)); + } + @Test + void emptyConfiguredInternalCredentialNeverMakesMetadataPublic() { + ReflectionTestUtils.setField(controller,"internalToken","");controller.initializeToken(); + var request=new com.baiyue.warehouse.dto.WarehouseMetadataBatch.Request(List.of(1L)); + assertThatThrownBy(()->controller.platformMetadata(null,request)).isInstanceOf(BizException.class); + assertThatThrownBy(()->controller.platformMetadata("",request)).isInstanceOf(BizException.class); + verifyNoInteractions(service); + } private static final String INTERNAL_TOKEN = "test-internal-service-token"; private final WarehouseApplicationService service = mock(WarehouseApplicationService.class); diff --git a/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseMetadataPersistenceTest.java b/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseMetadataPersistenceTest.java new file mode 100644 index 000000000..c9d69e1d2 --- /dev/null +++ b/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseMetadataPersistenceTest.java @@ -0,0 +1,29 @@ +package com.baiyue.warehouse.repository; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder; +import com.baiyue.warehouse.mapper.WarehouseMapper; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class WarehouseMetadataPersistenceTest { + @Test void batchReadIncludesInactiveButNeverPrivateOrDeletedWarehouses() throws Exception { + var ds=new JdbcDataSource();ds.setURL("jdbc:h2:mem:warehouse-metadata-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1"); + try(var c=ds.getConnection();var s=c.createStatement()) { + s.execute("CREATE TABLE wh_warehouse(id BIGINT,tenant_id BIGINT,warehouse_code VARCHAR,warehouse_name VARCHAR,warehouse_type VARCHAR,country_code VARCHAR,timezone VARCHAR,status VARCHAR,deleted TINYINT)"); + s.execute("INSERT INTO wh_warehouse VALUES(1,0,'W1','One','OVERSEAS','US','America/Los_Angeles','ACTIVE',0),(2,9,'PRIVATE','Private','OVERSEAS','US',NULL,'ACTIVE',0),(3,0,'OLD','Old','OVERSEAS','US',NULL,'INACTIVE',0),(4,0,'REMOVED','Removed','OVERSEAS','US',NULL,'INACTIVE',1)"); + } + var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.setEnvironment(new Environment("test",new JdbcTransactionFactory(),ds));config.addMapper(WarehouseMapper.class); + try(var session=new MybatisSqlSessionFactoryBuilder().build(config).openSession()) { + var repository=new WarehouseRepository();ReflectionTestUtils.setField(repository,"warehouses",session.getMapper(WarehouseMapper.class)); + var rows=repository.platformMetadata(List.of(1L,2L,3L,4L,5L)); + assertEquals(Set.of(1L,3L),new HashSet<>(rows.stream().map(r->r.getId()).toList())); + assertTrue(rows.stream().anyMatch(r->"INACTIVE".equals(r.getStatus()))); + } + } +} diff --git a/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseMetadataBatchTest.java b/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseMetadataBatchTest.java new file mode 100644 index 000000000..17e40eb24 --- /dev/null +++ b/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseMetadataBatchTest.java @@ -0,0 +1,33 @@ +package com.baiyue.warehouse.service; + +import com.baiyue.common.core.exception.BizException; +import com.baiyue.warehouse.entity.WarehouseEntity; +import com.baiyue.warehouse.repository.WarehouseRepository; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class WarehouseMetadataBatchTest { + final WarehouseRepository repository=mock(WarehouseRepository.class); + final WarehouseApplicationService service=new WarehouseApplicationService(); + WarehouseMetadataBatchTest(){ReflectionTestUtils.setField(service,"repository",repository);} + WarehouseEntity row(long id,long owner,String status,String zone) { + var row=new WarehouseEntity();row.setId(id);row.setTenantId(owner);row.setWarehouseCode("W-"+id); + row.setWarehouseName("Warehouse");row.setWarehouseType("OVERSEAS");row.setCountryCode("US");row.setTimezone(zone);row.setStatus(status);return row; + } + @Test void returnsInactivePublicMetadataAndMissingIdsWithoutInventingTimeZones() { + var ids=List.of(3L,1L,2L,4L); + when(repository.platformMetadata(ids)).thenReturn(List.of(row(1,0,"ACTIVE",null),row(2,9,"ACTIVE","America/New_York"),row(3,0,"INACTIVE","UTC-5"))); + var response=service.platformMetadata(ids); + assertEquals(List.of(3L,1L),response.records().stream().map(r->r.id()).toList()); + assertEquals("INACTIVE",response.records().get(0).status());assertEquals("UTC-5",response.records().get(0).timezone()); + assertNull(response.records().get(1).timezone());assertEquals(List.of(2L,4L),response.missingIds());verify(repository,times(1)).platformMetadata(ids); + } + @Test void rejectsEmptyDuplicateNonPositiveNullAndOversizedRequestsBeforeQuerying() { + for(List ids:Arrays.asList(null,List.of(),List.of(1L,1L),List.of(0L),Arrays.asList(1L,null),Collections.nCopies(2001,1L))) + assertThrows(BizException.class,()->service.platformMetadata(ids)); + verifyNoInteractions(repository); + } +}