feat(selfop): reconstruct historical inventory and publish qualified turnover

This commit is contained in:
2026-10-15 13:30:37 +08:00
parent d81d9cbda4
commit 99215bbb79
35 changed files with 1652 additions and 106 deletions
@@ -87,6 +87,15 @@ public class LingxingInternalController {
tokenGuard.verify(token);return ApiResponse.success(client.queryInventoryFlow(request,context(context)));
}
@Operation(summary = "读取固定单仓最多一天的全部货主品质库存流水单页")
@PostMapping("/inventory-flow-page/query")
public ApiResponse<QueryPage<InventoryFlow>> inventoryFlowPage(
@RequestHeader(value="X-Internal-Service-Token",required=false) String token,
@RequestHeader(value="X-Lingxing-Egress-Context",required=false) String context,
@Valid @RequestBody InventoryFlowPageRequest request) {
tokenGuard.verify(token);return ApiResponse.success(client.queryInventoryFlowPage(request,context(context)));
}
@Operation(summary = "读取单仓全部货主、全部品质且保留零库存的产品库存单页")
@PostMapping("/product-inventory/query")
public ApiResponse<QueryPage<ProductStock>> inventory(
@@ -23,9 +23,12 @@ public final class LingxingWmsDtos {
public record InventoryFlowRequest(@NotBlank @Size(max=64) String warehouseCode,
@NotBlank @Size(max=64) String customerCode, @NotBlank @Size(max=128) String sku,
@NotNull LocalDateTime startTime, @NotNull LocalDateTime endTime) { }
public record InventoryFlowPageRequest(@NotBlank @Size(max=64) String warehouseCode,
@NotNull LocalDateTime startTime,@NotNull LocalDateTime endTime,
@Min(1) int current,@Min(1) @Max(1000) int size) { }
public record InventoryFlow(String id, String customerCode, String whCode, String productSku,
String cellNo, String innerBatchNo, String billNo, String billType, Integer productQuality,
BigDecimal bookQty, String bookChangQty, BigDecimal closeQty, String createTime) { }
BigDecimal bookQty, String bookChangQty, BigDecimal closeQty, String createTime, String sourceNo) { }
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,
@@ -173,16 +173,35 @@ public final class LingxingWmsClient {
body.put("barcodeAccurate",request.sku());body.put("startTime",SOURCE_TIME.format(request.startTime()));
body.put("endTime",SOURCE_TIME.format(request.endTime()));body.put("sortName","createTime");body.put("sortOrder","asc");
return execute("/openapi/v2/stock/stockCellBook/page",body,context,Operation.INVENTORY_FLOW,
(data,metadata)->mappedPage(data,1,1000,"current","size",false,metadata,row->{
(data,metadata)->mappedPage(data,1,1000,"current","size",true,metadata,row->{
if(!request.warehouseCode().equals(requiredText(row,"whCode",64))
||!request.customerCode().equals(requiredText(row,"customerCode",64))
||!request.sku().equals(requiredText(row,"productSku",128)))throw invalidResponse("FLOW_IDENTITY_MISMATCH",200);
JsonNode id=row.get("id");
if(id==null||!id.isIntegralNumber())throw invalidResponse("FLOW_ID_MISSING",200);
return new InventoryFlow(id.asText(),request.customerCode(),request.warehouseCode(),request.sku(),
String id=requiredText(row,"id",64);
return new InventoryFlow(id,request.customerCode(),request.warehouseCode(),request.sku(),
optionalText(row,"cellNo"),optionalText(row,"innerBatchNo"),optionalText(row,"billNo"),optionalText(row,"billType"),
requiredInt(row.get("productQuality"),"PRODUCT_QUALITY"),quantity(row.get("bookQty")),
optionalText(row,"bookChangQty"),quantity(row.get("closeQty")),optionalText(row,"createTime"));
optionalText(row,"bookChangQty"),quantity(row.get("closeQty")),optionalText(row,"createTime"),optionalText(row,"sourceNo"));
}));
}
/** One fixed warehouse interval page. Scheduling and coverage belong to Lastleg. */
public QueryPage<InventoryFlow> queryInventoryFlowPage(InventoryFlowPageRequest request,EgressContext context) {
if(request==null) throw badRequest();
requireInput(request.warehouseCode(),64);validatePagination(request.current(),request.size(),1000);
if(request.startTime()==null || request.endTime()==null || !request.startTime().isBefore(request.endTime())
|| request.endTime().isAfter(request.startTime().plusDays(1))) throw badRequest();
Map<String,Object> body=pagination(request.current(),request.size());
body.put("whCode",request.warehouseCode());body.put("startTime",SOURCE_TIME.format(request.startTime()));
body.put("endTime",SOURCE_TIME.format(request.endTime()));body.put("sortName","createTime");body.put("sortOrder","asc");
return execute("/openapi/v2/stock/stockCellBook/page",body,context,Operation.INVENTORY_FLOW,
(data,metadata)->mappedPage(data,request.current(),request.size(),"current","size",true,metadata,row->{
String warehouse=requiredText(row,"whCode",64);
if(!request.warehouseCode().equals(warehouse)) throw invalidResponse("FLOW_IDENTITY_MISMATCH",200);
return new InventoryFlow(requiredText(row,"id",64),requiredText(row,"customerCode",64),warehouse,
requiredText(row,"productSku",128),optionalText(row,"cellNo"),optionalText(row,"innerBatchNo"),
optionalText(row,"billNo"),optionalText(row,"billType"),requiredInt(row.get("productQuality"),"PRODUCT_QUALITY"),
quantity(row.get("bookQty")),optionalText(row,"bookChangQty"),quantity(row.get("closeQty")),optionalText(row,"createTime"),optionalText(row,"sourceNo"));
}));
}
@@ -343,7 +362,7 @@ public final class LingxingWmsClient {
}
private PageResult orders(JsonNode data, int current, int size, String exactDeliveryNo, AttemptMetadata metadata) {
QueryPage<OrderSummary> page = mappedPage(data, current, size, "current", "size", true, metadata, record -> {
QueryPage<OrderSummary> page = mappedPage(data, current, size, "current", "size", size == 100, metadata, record -> {
String deliveryNo = requiredText(record, "deliveryNo", 128);
if (exactDeliveryNo != null && !exactDeliveryNo.equals(deliveryNo)) throw invalidResponse("LOCATE_DELIVERY_NO_MISMATCH", 200);
int status = requiredInt(record.get("status"), "STATUS");
@@ -367,7 +386,7 @@ public final class LingxingWmsClient {
}
private <T> QueryPage<T> mappedPage(JsonNode data, int current, int size, String currentField,
String sizeField, boolean outbound, AttemptMetadata metadata, Function<JsonNode, T> projection) {
String sizeField, boolean allowEmptyDefaultSize, AttemptMetadata metadata, Function<JsonNode, T> projection) {
JsonNode records = data.get("records");
if (records == null || !records.isArray()) throw invalidResponse("RECORDS_TYPE_" + nodeType(records), 200);
if (records.size() > size) throw invalidResponse("RECORDS_OVER_PAGE_SIZE", 200);
@@ -377,7 +396,8 @@ public final class LingxingWmsClient {
int sourceSize = requiredInt(data.get(sizeField), "SIZE");
int pages = requiredInt(data.get("pages"), "PAGES");
if (sourceCurrent != current) throw invalidResponse("CURRENT_MISMATCH", 200);
boolean emptyException = outbound && current == 1 && size == 100 && sourceCurrent == 1
// Verified for outbound size 100 and the bounded inventory-flow query only.
boolean emptyException = allowEmptyDefaultSize && current == 1 && sourceCurrent == 1
&& sourceSize == 10 && total == 0 && pages == 0 && records.isEmpty();
if (sourceSize != size && !emptyException) throw invalidResponse("SIZE_MISMATCH", 200);
long expectedPages = total / size + (total % size == 0 ? 0 : 1);
@@ -51,6 +51,34 @@ x-lastleg-egress-coordination:
security:
- InternalServiceToken: []
paths:
/internal/integrations/lingxing/wms/inventory-flow-page/query:
post:
summary: One frozen warehouse inventory-flow page
description: >-
Shared INVENTORY_FLOW permit, one upstream request, no automatic retry. Fixed warehouse and
inclusive Beijing start/end bounds at most one day, current>=1, size 1..1000, sorted by createTime asc.
Customer/SKU filters are absent. Lastleg validates persisted task/page binding, page counts,
duplicate IDs and ordering before admitting coverage. Only quantity/identity whitelist fields return.
Empty first-page current=1,size=10,total=0,pages=0,records=[] is normalized to the requested size.
parameters:
- {$ref: '#/components/parameters/LingxingEgressContext'}
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [warehouseCode, startTime, endTime, current, size]
properties:
warehouseCode: {type: string, minLength: 1, maxLength: 64}
startTime: {type: string, format: date-time, description: Beijing local datetime without offset}
endTime: {type: string, format: date-time, description: Inclusive bound, at most one day after start}
current: {type: integer, minimum: 1}
size: {type: integer, minimum: 1, maximum: 1000}
responses:
'200':
description: Envelope of QueryPage with current/size/total/pages/records and source pagination evidence, or sanitized ProviderFailure. Rows retain string id, customerCode, whCode, productSku, cellNo, innerBatchNo, billNo, sourceNo, billType, productQuality, numeric bookQty/closeQty and string bookChangQty/createTime.
/internal/integrations/lingxing/wms/inventory-flow/query:
post:
summary: One SKU inventory-flow diagnostic page
@@ -59,6 +87,9 @@ paths:
at most 31 days, current=1 and size=1000 fixed. No automatic next page or retry. Source quality and signed
quantities are preserved, not translated to inventory-snapshot quality. Source 11008 only rejects this optional
operation; authentication, rate-limit and uncertain-result account protection remain effective.
Source movement IDs must be strings. An empty first response with current=1, size=10, total=0,
pages=0 and records=[] is normalized to requested size=1000 while retaining sourceSize=10.
Other pagination mismatches remain errors.
parameters:
- {$ref: '#/components/parameters/LingxingEgressContext'}
requestBody:
@@ -76,7 +107,7 @@ paths:
endTime: {type: string, format: date-time, description: Beijing local datetime without offset}
responses:
'200':
description: Standard envelope with QueryPage or sanitized ProviderFailure. Flow rows retain id as string, customerCode, whCode, productSku, cellNo, innerBatchNo, billNo, billType, productQuality, numeric bookQty/closeQty, string bookChangQty/createTime. Unknown source fields remain null; no calculated inventory is published.
description: Standard envelope with QueryPage or sanitized ProviderFailure. Flow rows retain id as string, customerCode, whCode, productSku, cellNo, innerBatchNo, billNo, sourceNo, billType, productQuality, numeric bookQty/closeQty, string bookChangQty/createTime. Unknown source fields remain null; no calculated inventory is published.
/internal/integrations/lingxing/wms/customers/query:
post:
summary: Query one page of customers using the existing account egress permit