feat(warehouse): provide bounded internal historical metadata batch
This commit is contained in:
@@ -12,6 +12,11 @@
|
||||
<artifactId>warehouse-service</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
|
||||
+9
-1
@@ -28,6 +28,14 @@ public class InternalWarehouseController {
|
||||
private String internalToken;
|
||||
private byte[] expected;
|
||||
|
||||
@PostMapping("/platform-metadata/batch")
|
||||
public ApiResponse<com.baiyue.warehouse.dto.WarehouseMetadataBatch.Response> 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, "内部服务凭证无效");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Item> records,List<Long> missingIds) { }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -46,6 +46,14 @@ public class WarehouseRepository {
|
||||
.eq(WarehouseEntity::getId, id));
|
||||
}
|
||||
|
||||
public List<WarehouseEntity> platformMetadata(List<Long> ids) {
|
||||
return warehouses.selectList(new LambdaQueryWrapper<WarehouseEntity>()
|
||||
.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<WarehouseEntity>()
|
||||
.eq(WarehouseEntity::getTenantId, tenantId)
|
||||
|
||||
@@ -28,6 +28,26 @@ public class WarehouseApplicationService {
|
||||
@Resource
|
||||
private WarehouseRepository repository;
|
||||
|
||||
@Transactional(readOnly=true)
|
||||
public com.baiyue.warehouse.dto.WarehouseMetadataBatch.Response platformMetadata(List<Long> 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<Long,WarehouseEntity>();
|
||||
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<com.baiyue.warehouse.dto.WarehouseMetadataBatch.Item>();
|
||||
var missing=new java.util.ArrayList<Long>();
|
||||
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(
|
||||
|
||||
@@ -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: 分页查询启用的公共海外收货仓
|
||||
|
||||
+16
@@ -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);
|
||||
|
||||
+29
@@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Long> ids:Arrays.asList(null,List.<Long>of(),List.of(1L,1L),List.of(0L),Arrays.asList(1L,null),Collections.nCopies(2001,1L)))
|
||||
assertThrows(BizException.class,()->service.platformMetadata(ids));
|
||||
verifyNoInteractions(repository);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user