feat(selfop): sync versioned owner and product directories
This commit is contained in:
+18
@@ -88,6 +88,24 @@ public class LingxingInternalController {
|
||||
return ApiResponse.success(client.queryInventory(request, context(context)));
|
||||
}
|
||||
|
||||
@PostMapping("/customers/query")
|
||||
public ApiResponse<QueryPage<Customer>> customers(
|
||||
@RequestHeader(value="X-Internal-Service-Token",required=false) String token,
|
||||
@RequestHeader(value="X-Lingxing-Egress-Context",required=false) String context,
|
||||
@Valid @RequestBody WarehouseQueryRequest request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(client.queryCustomers(request,context(context)));
|
||||
}
|
||||
|
||||
@PostMapping("/products/query")
|
||||
public ApiResponse<QueryPage<Product>> products(
|
||||
@RequestHeader(value="X-Internal-Service-Token",required=false) String token,
|
||||
@RequestHeader(value="X-Lingxing-Egress-Context",required=false) String context,
|
||||
@Valid @RequestBody ProductQueryRequest request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(client.queryProducts(request,context(context)));
|
||||
}
|
||||
|
||||
private EgressContext context(String header) {
|
||||
if (header == null || header.isBlank() || header.length() > 2048 || !header.chars().allMatch(c -> c >= 32 && c <= 126))
|
||||
throw invalidContext();
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import java.time.Instant;
|
||||
public final class LingxingEgressDtos {
|
||||
private LingxingEgressDtos() { }
|
||||
public enum CollectionMode { OFF, VALIDATION, BUSINESS }
|
||||
public enum Operation { WAREHOUSES, OUTBOUND_QUERY, OUTBOUND_LOCATE, OUTBOUND_DETAIL, PRODUCT_INVENTORY }
|
||||
public enum Operation { WAREHOUSES, OUTBOUND_QUERY, OUTBOUND_LOCATE, OUTBOUND_DETAIL, PRODUCT_INVENTORY, CUSTOMERS, PRODUCTS }
|
||||
public record EgressContext(String sourceAccount, CollectionMode executionPurpose, String rootTaskId,
|
||||
String sessionId, String attemptId, String exclusiveGrantId) {
|
||||
public boolean wellFormed() {
|
||||
|
||||
+5
@@ -19,6 +19,11 @@ public final class LingxingWmsDtos {
|
||||
@Min(1) int current, @Min(1) @Max(100) int size) { }
|
||||
|
||||
public record WarehouseQueryRequest(@Min(1) int current, @Min(1) @Max(1000) int size) { }
|
||||
public record ProductQueryRequest(@Min(1) int current, @Min(1) @Max(100) int size) { }
|
||||
public record Customer(String customerCode, String customerName, String status) { }
|
||||
public record Product(String customerCode, String sku, String productName,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, String sizeUnit,
|
||||
BigDecimal wmsLength, BigDecimal wmsWidth, BigDecimal wmsHeight, String wmsSizeUnit) { }
|
||||
public record LocateRequest(@NotBlank @Size(max=128) String deliveryNo,
|
||||
@Min(1) int current, @Min(1) @Max(1000) int size) { }
|
||||
public record DetailRequest(@NotBlank @Size(max=64) String warehouseCode,
|
||||
|
||||
+18
@@ -133,6 +133,24 @@ public final class LingxingWmsClient {
|
||||
optionalText(data, "outboundTime"), lines, 200, clock.instant(), metadata);
|
||||
}
|
||||
|
||||
public QueryPage<Customer> queryCustomers(WarehouseQueryRequest request, EgressContext context) {
|
||||
if (request == null) throw badRequest();
|
||||
validatePagination(request.current(), request.size(), 1000);
|
||||
return execute("/openapi/v2/customer/list", pagination(request.current(), request.size()), context, Operation.CUSTOMERS,
|
||||
(data, metadata) -> mappedPage(data, request.current(), request.size(), "current", "size", false, metadata,
|
||||
row -> new Customer(requiredText(row,"customerCode",64), optionalText(row,"customerName"), optionalText(row,"status"))));
|
||||
}
|
||||
|
||||
public QueryPage<Product> queryProducts(ProductQueryRequest request, EgressContext context) {
|
||||
if (request == null) throw badRequest();
|
||||
validatePagination(request.current(), request.size(), 100);
|
||||
return execute("/openapi/v2/product/page", pagination(request.current(), request.size()), context, Operation.PRODUCTS,
|
||||
(data, metadata) -> mappedPage(data, request.current(), request.size(), "current", "size", false, metadata,
|
||||
row -> new Product(requiredText(row,"customerCode",64), requiredText(row,"sku",128), optionalText(row,"productName"),
|
||||
quantity(row.get("length")),quantity(row.get("width")),quantity(row.get("height")),optionalText(row,"sizeUnit"),
|
||||
quantity(row.get("wmsLength")),quantity(row.get("wmsWidth")),quantity(row.get("wmsHeight")),optionalText(row,"wmsSizeUnit"))));
|
||||
}
|
||||
|
||||
public QueryPage<ProductStock> queryInventory(InventoryQueryRequest request, EgressContext context) {
|
||||
if (request == null) throw badRequest();
|
||||
requireInput(request.warehouseCode(), 64);
|
||||
|
||||
@@ -51,6 +51,40 @@ x-lastleg-egress-coordination:
|
||||
security:
|
||||
- InternalServiceToken: []
|
||||
paths:
|
||||
/internal/integrations/lingxing/wms/customers/query:
|
||||
post:
|
||||
summary: Query one page of customers using the existing account egress permit
|
||||
description: Calls /openapi/v2/customer/list. BUSINESS MASTER_DATA tasks only; no retries or separate egress path. Dimensions and units remain separate; blanks are null.
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/LingxingEgressContext'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/WarehouseQueryRequest'}
|
||||
responses:
|
||||
'200':
|
||||
description: Standard envelope with one page or sanitized ProviderFailure.
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CustomerResponse'}
|
||||
/internal/integrations/lingxing/wms/products/query:
|
||||
post:
|
||||
summary: Query one page of products using the existing account egress permit
|
||||
description: Calls /openapi/v2/product/page. BUSINESS MASTER_DATA tasks only; no retries or separate egress path. Dimensions and units remain separate; blanks are null.
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/LingxingEgressContext'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ProductQueryRequest'}
|
||||
responses:
|
||||
'200':
|
||||
description: Standard envelope with one page or sanitized ProviderFailure.
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ProductResponse'}
|
||||
/internal/integrations/lingxing/wms/egress/runtime:
|
||||
get:
|
||||
operationId: lingxingEgressRuntime
|
||||
@@ -175,6 +209,68 @@ components:
|
||||
in: header
|
||||
name: X-Internal-Service-Token
|
||||
schemas:
|
||||
CustomerPage:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/Pagination'}
|
||||
- type: object
|
||||
required: [records]
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/Customer'}}
|
||||
CustomerResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/Envelope'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
nullable: true
|
||||
oneOf:
|
||||
- {$ref: '#/components/schemas/CustomerPage'}
|
||||
- {$ref: '#/components/schemas/ProviderFailure'}
|
||||
ProductPage:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/Pagination'}
|
||||
- type: object
|
||||
required: [records]
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/Product'}}
|
||||
ProductResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/Envelope'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
nullable: true
|
||||
oneOf:
|
||||
- {$ref: '#/components/schemas/ProductPage'}
|
||||
- {$ref: '#/components/schemas/ProviderFailure'}
|
||||
ProductQueryRequest:
|
||||
type: object
|
||||
required: [current, size]
|
||||
properties:
|
||||
current: {type: integer, minimum: 1}
|
||||
size: {type: integer, minimum: 1, maximum: 100}
|
||||
Customer:
|
||||
type: object
|
||||
required: [customerCode]
|
||||
properties:
|
||||
customerCode: {type: string}
|
||||
customerName: {type: string, nullable: true}
|
||||
status: {type: string, nullable: true}
|
||||
Product:
|
||||
type: object
|
||||
required: [customerCode, sku]
|
||||
properties:
|
||||
customerCode: {type: string}
|
||||
sku: {type: string}
|
||||
productName: {type: string, nullable: true}
|
||||
length: {type: number, nullable: true}
|
||||
width: {type: number, nullable: true}
|
||||
height: {type: number, nullable: true}
|
||||
wmsLength: {type: number, nullable: true}
|
||||
wmsWidth: {type: number, nullable: true}
|
||||
wmsHeight: {type: number, nullable: true}
|
||||
sizeUnit: {type: string, nullable: true}
|
||||
wmsSizeUnit: {type: string, nullable: true}
|
||||
EgressRuntimeSnapshot:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -219,7 +315,7 @@ components:
|
||||
{"current":1,"size":2,"whCode":"WH-DEMO","isHideInventory":0}
|
||||
EgressOperation:
|
||||
type: string
|
||||
enum: [WAREHOUSES, OUTBOUND_QUERY, OUTBOUND_LOCATE, OUTBOUND_DETAIL, PRODUCT_INVENTORY]
|
||||
enum: [WAREHOUSES, OUTBOUND_QUERY, OUTBOUND_LOCATE, OUTBOUND_DETAIL, PRODUCT_INVENTORY, CUSTOMERS, PRODUCTS]
|
||||
EgressMetadata:
|
||||
type: object
|
||||
required: [attemptId, actualHttpAttempts, accountPaused]
|
||||
|
||||
+17
@@ -22,6 +22,23 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/** Synthetic loopback transport only; no supplier or application services. */
|
||||
class LingxingWmsClientTest {
|
||||
@Test void customerDirectoryKeepsNamesAndUsesAccountGate() {
|
||||
response.set(page("current","size",1,1000,1,1,"{\"customerCode\":\"C\",\"customerName\":\"示例货主\",\"status\":\"0\",\"telephoneNo\":\"not-forwarded\"}"));
|
||||
var result=client().queryCustomers(new WarehouseQueryRequest(1,1000),context());
|
||||
assertThat(path.get()).isEqualTo("/openapi/v2/customer/list");
|
||||
assertThat(result.records()).containsExactly(new Customer("C","示例货主","0"));
|
||||
assertThat(completion.get().operation()).isEqualTo(Operation.CUSTOMERS);
|
||||
}
|
||||
@Test void productDirectoryRetainsDeclaredDimensionsAndUnknownMeasurements() {
|
||||
response.set(page("current","size",1,100,1,1,"{\"customerCode\":\"C\",\"sku\":\"SKU\",\"productName\":\"商品\",\"length\":5,\"width\":11,\"height\":5,\"sizeUnit\":\"cm\",\"wmsLength\":\"\",\"wmsWidth\":\"\",\"wmsHeight\":\"\",\"wmsSizeUnit\":\"\"}"));
|
||||
var result=client().queryProducts(new ProductQueryRequest(1,100),context());
|
||||
assertThat(result.records().get(0).length()).isEqualByComparingTo("5");
|
||||
assertThat(result.records().get(0).wmsLength()).isNull();
|
||||
assertThat(result.records().get(0).sku()).isEqualTo("SKU");
|
||||
assertThat(completion.get().operation()).isEqualTo(Operation.PRODUCTS);
|
||||
assertThatThrownBy(()->client().queryProducts(new ProductQueryRequest(1,101),context())).isInstanceOf(LingxingWmsException.class);
|
||||
assertThat(calls.get()).isEqualTo(1);
|
||||
}
|
||||
private static final Instant NOW = Instant.parse("2026-10-12T00:00:00Z");
|
||||
private static final String PRIVATE = "private-secret@example.test";
|
||||
private static final String ORDER = """
|
||||
|
||||
Reference in New Issue
Block a user