This commit is contained in:
2026-08-12 14:08:29 +08:00
parent 4375beaa13
commit aaa38f825a
5 changed files with 485 additions and 7 deletions
@@ -64,6 +64,58 @@ paths:
schema: {$ref: '#/components/schemas/OrderApprovedInboundCommand'}
responses:
'200': {description: 成功或业务失败;相同客户和订单重放返回已有入库单 ID}
/internal/firstleg/v1/inbounds/unified-orders/query:
post:
summary: 按统一订单条件分页查询履约摘要(Order 内部)
description: >-
mainStatus 接受六态产品状态;历史 WAIT_SPLIT 仅作为兼容筛选值,按 INBOUNDED 范围查询。
所有响应 mainStatus 均归并为六态产品状态,不返回 WAIT_SPLIT。
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/UnifiedInboundQuery'}
responses:
'200':
description: 成功或业务失败,按响应体 code 判断
content:
application/json:
schema: {$ref: '#/components/schemas/UnifiedInboundPageEnvelope'}
/internal/firstleg/v1/inbounds/unified-orders/summaries:
post:
summary: 按订单 ID 批量查询统一履约摘要(Order 内部)
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/UnifiedInboundBatchRequest'}
responses:
'200':
description: 成功或业务失败,按响应体 code 判断
content:
application/json:
schema: {$ref: '#/components/schemas/UnifiedInboundListEnvelope'}
/internal/firstleg/v1/inbounds/unified-orders/detail:
post:
summary: 按订单 ID 查询统一履约详情(Order 内部)
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 始终使用六态产品状态。
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/UnifiedInboundDetailRequest'}
responses:
'200':
description: 成功或业务失败;找不到对应入库单时 data 为 null
content:
application/json:
schema: {$ref: '#/components/schemas/UnifiedInboundDetailEnvelope'}
/api/platform/v1/first-leg/tasks:
get:
summary: 分页查询履约任务
@@ -528,6 +580,198 @@ components:
required: true
schema: {type: string, pattern: '^[1-9][0-9]*$'}
schemas:
UnifiedMainStatus:
type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED]
description: 产品主状态;历史内部状态 WAIT_SPLIT 在响应中统一归并为 INBOUNDED
UnifiedInboundQuery:
type: object
properties:
tenantId: {type: integer, format: int64, nullable: true}
keyword: {type: string, maxLength: 64, nullable: true}
orderNo: {type: string, maxLength: 64, nullable: true}
mainStatus:
type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED, WAIT_SPLIT]
nullable: true
description: WAIT_SPLIT 仅为历史兼容筛选值,等价于 INBOUNDED
customerId: {type: integer, format: int64, nullable: true}
receivingWarehouseId: {type: integer, format: int64, nullable: true}
destinationWarehouseId: {type: integer, format: int64, nullable: true}
inboundCode: {type: string, maxLength: 64, nullable: true}
internalOrderStatus: {type: string, maxLength: 32, nullable: true}
expectedArrivalDate: {type: string, format: date, nullable: true}
feeReviewStatus: {type: string, maxLength: 32, nullable: true}
channelName: {type: string, maxLength: 128, nullable: true}
containerNo: {type: string, maxLength: 64, nullable: true}
cabinetNo: {type: string, maxLength: 64, nullable: true}
sailingDate: {type: string, format: date, nullable: true}
loaded: {type: boolean, nullable: true}
fullContainer: {type: boolean, nullable: true}
measurementReviewStatus: {type: string, maxLength: 32, nullable: true}
candidateOrderIds: {type: array, nullable: true, items: {type: integer, format: int64}}
createdFrom: {type: string, format: date-time, nullable: true}
createdTo: {type: string, format: date-time, nullable: true}
page: {type: integer, format: int64, minimum: 1, default: 1}
size: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
UnifiedInboundBatchRequest:
type: object
required: [orderIds]
properties:
tenantId: {type: integer, format: int64, nullable: true}
orderIds:
type: array
minItems: 1
maxItems: 200
items: {type: integer, format: int64, minimum: 1}
UnifiedInboundDetailRequest:
type: object
required: [tenantId, orderId]
properties:
tenantId: {type: integer, format: int64, minimum: 1}
orderId: {type: integer, format: int64, minimum: 1}
UnifiedInboundPageEnvelope:
type: object
properties:
code: {type: string, example: '200'}
message: {type: string}
data:
type: object
properties:
records: {type: array, items: {$ref: '#/components/schemas/UnifiedInboundSummary'}}
page: {type: integer, format: int64, minimum: 1}
size: {type: integer, format: int64, minimum: 1}
total: {type: integer, format: int64, minimum: 0}
pages: {type: integer, format: int64, minimum: 0}
UnifiedInboundListEnvelope:
type: object
properties:
code: {type: string, example: '200'}
message: {type: string}
data: {type: array, items: {$ref: '#/components/schemas/UnifiedInboundSummary'}}
UnifiedInboundDetailEnvelope:
type: object
properties:
code: {type: string, example: '200'}
message: {type: string}
data:
allOf: [{$ref: '#/components/schemas/UnifiedInboundDetail'}]
nullable: true
UnifiedInboundDetail:
type: object
properties:
summary: {$ref: '#/components/schemas/UnifiedInboundSummary'}
timeline:
type: array
items:
type: object
properties:
source: {type: string}
eventType: {type: string}
fromStatus: {type: string, nullable: true}
toStatus: {type: string, nullable: true}
action: {type: string, nullable: true}
remark: {type: string, nullable: true}
operatorId: {type: integer, format: int64, nullable: true}
operatorName: {type: string, nullable: true}
occurredAt: {type: string, format: date-time, nullable: true}
UnifiedInboundSummary:
type: object
properties:
inbound: {$ref: '#/components/schemas/UnifiedInboundOrderSummary'}
mainStatus: {$ref: '#/components/schemas/UnifiedMainStatus'}
nextAction: {type: string, nullable: true}
preload: {$ref: '#/components/schemas/UnifiedPreloadSummary'}
loading: {$ref: '#/components/schemas/UnifiedLoadingSummary'}
delivery: {$ref: '#/components/schemas/UnifiedDeliverySummary'}
UnifiedInboundOrderSummary:
type: object
description: InboundOrderSummary 的内部统一订单投影
properties:
id: {type: integer, format: int64}
tenantId: {type: integer, format: int64}
orderId: {type: integer, format: int64, nullable: true}
orderNo: {type: string, nullable: true}
inboundCode: {type: string, nullable: true}
processStatus: {type: string, nullable: true, description: Firstleg 内部状态,历史数据可能为 WAIT_SPLIT}
orderStatusSnapshot: {type: string, nullable: true}
measureReviewStatus: {type: string, nullable: true}
feeStatus: {type: string, nullable: true}
feeReviewStatus: {type: string, nullable: true}
notificationStatus: {type: string, nullable: true}
version: {type: integer, nullable: true}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
customerId: {type: integer, format: int64, nullable: true}
customerName: {type: string, nullable: true}
companyId: {type: integer, format: int64, nullable: true}
companyName: {type: string, nullable: true}
warehouseId: {type: integer, format: int64, nullable: true}
warehouseName: {type: string, nullable: true}
deliveryWarehouseId: {type: integer, format: int64, nullable: true}
deliveryWarehouseName: {type: string, nullable: true}
businessType: {type: string, nullable: true}
channelCode: {type: string, nullable: true}
channelName: {type: string, nullable: true}
taxMode: {type: string, nullable: true}
pricingTaxMode: {type: string, nullable: true}
containerMode: {type: string, nullable: true}
customsInspection: {type: boolean, nullable: true}
destinationType: {type: string, nullable: true}
destinationWarehouseId: {type: integer, format: int64, nullable: true}
destinationCode: {type: string, nullable: true}
destinationName: {type: string, nullable: true}
holdFlag: {type: boolean, nullable: true}
currentHoldReason: {type: string, nullable: true}
totalPackageCount: {type: integer, nullable: true}
locationCodes: {type: string, nullable: true}
actualGrossWeightKg: {type: number, nullable: true}
actualNetWeightKg: {type: number, nullable: true}
actualVolumeCbm: {type: number, nullable: true}
volumeWeightKg: {type: number, nullable: true}
chargeableWeightKg: {type: number, nullable: true}
chargeableVolumeCbm: {type: number, nullable: true}
palletCount: {type: integer, nullable: true}
measureCompletedAt: {type: string, format: date-time, nullable: true}
inboundedAt: {type: string, format: date-time, nullable: true}
expressCompanyCode: {type: string, nullable: true}
expressCompanyName: {type: string, nullable: true}
loadingStatus: {type: string, nullable: true}
containerNo: {type: string, nullable: true}
cabinetNo: {type: string, nullable: true}
vesselName: {type: string, nullable: true}
voyageNo: {type: string, nullable: true}
sailingAt: {type: string, format: date-time, nullable: true}
portArrivalAt: {type: string, format: date-time, nullable: true}
expressTrackingNo: {type: string, nullable: true}
createdAt: {type: string, format: date-time, nullable: true}
updatedAt: {type: string, format: date-time, nullable: true}
UnifiedPreloadSummary:
type: object
nullable: true
properties:
status: {type: string, nullable: true}
version: {type: integer, nullable: true}
itemCount: {type: integer, nullable: true}
updatedAt: {type: string, format: date-time, nullable: true}
UnifiedLoadingSummary:
type: object
nullable: true
properties:
status: {type: string, nullable: true}
loadingVersion: {type: integer, nullable: true, description: 当前为 pickingVersion 兼容别名}
pickingVersion: {type: integer, nullable: true}
expectedCount: {type: integer, nullable: true}
pickedCount: {type: integer, nullable: true}
evidenceCount: {type: integer, nullable: true}
sealNo: {type: string, nullable: true}
updatedAt: {type: string, format: date-time, nullable: true}
UnifiedDeliverySummary:
type: object
nullable: true
properties:
status: {type: string, nullable: true}
completedAt: {type: string, format: date-time, nullable: true}
trackingNo: {type: string, nullable: true}
MojiaMeasurementUploadEnvelope:
type: object
properties:
@@ -41,6 +41,8 @@ class InboundOrderControllerContractTest {
String taskList = openapi.substring(
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
assertThat(inboundList)
.doesNotContain("WAIT_SPLIT");
assertThat(openapi)
.contains("/api/platform/inbounds:")
.contains("/api/platform/inbounds/{inboundId}:")
@@ -53,8 +55,7 @@ class InboundOrderControllerContractTest {
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
.contains("platform:firstleg:inbound:list")
.doesNotContain("WAIT_CUSTOMER_CONFIRM")
.doesNotContain("WAIT_SPLIT");
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
assertThat(taskList).doesNotContain("InboundOrderPageEnvelope");
}
@@ -0,0 +1,56 @@
package com.baiyue.firstleg.controller;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class UnifiedInboundOpenApiContractTest {
@Test
void documentsInternalUnifiedOrderEndpointsAndLegacyWaitSplitMapping() throws Exception {
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> document = stringMap(new Yaml().load(yaml));
Map<String, Object> paths = stringMap(document.get("paths"));
assertThat(paths).containsKeys(
"/internal/firstleg/v1/inbounds/unified-orders/query",
"/internal/firstleg/v1/inbounds/unified-orders/summaries",
"/internal/firstleg/v1/inbounds/unified-orders/detail");
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
assertThat(stringList(queryStatus.get("enum"))).contains("WAIT_SPLIT");
assertThat(queryStatus.get("description").toString()).contains("INBOUNDED");
Map<String, Object> inbound = stringMap(schemas.get("UnifiedInboundOrderSummary"));
assertThat(stringMap(inbound.get("properties"))).containsKeys(
"id", "tenantId", "orderId", "inboundCode", "processStatus", "version",
"customerId", "warehouseId", "deliveryWarehouseId", "totalPackageCount",
"businessType", "channelCode", "pricingTaxMode", "containerMode", "destinationType",
"holdFlag", "actualGrossWeightKg", "actualNetWeightKg", "actualVolumeCbm",
"volumeWeightKg", "chargeableWeightKg", "chargeableVolumeCbm", "palletCount",
"inboundedAt", "expressCompanyCode", "expressCompanyName", "loadingStatus", "containerNo",
"cabinetNo", "sailingAt", "portArrivalAt", "createdAt", "updatedAt");
}
@SuppressWarnings("unchecked")
private static Map<String, Object> stringMap(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<String> stringList(Object value) {
return (List<String>) value;
}
}