From cd34cd426e96cad3ba8f7c3c6c947e2791a27b00 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 20 Aug 2026 15:25:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/InternalBusinessFileService.java | 3 +- .../InternalBusinessFileServiceTest.java | 8 +++ .../OverseasContainerEventConsumerTest.java | 2 +- .../controller/LastlegHealthController.java | 8 +-- .../controller/LastlegOrderController.java | 20 ++++-- .../DeliveryTransferController.java | 13 +++- .../controller/DispatchController.java | 23 +++--- .../lastleg/dto/LastlegOrderSummary.java | 2 +- .../controller/InboundPlanController.java | 17 +++-- .../InboundUnloadCompletedOutboxService.java | 7 +- .../InventoryCommandController.java | 23 +++--- .../controller/InventoryQueryController.java | 20 ++++-- ...boundUnloadCompletedOutboxServiceTest.java | 71 +++++++++++++++++++ .../service/LoadingCompletedConsumerTest.java | 2 +- 14 files changed, 170 insertions(+), 49 deletions(-) create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxServiceTest.java diff --git a/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java b/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java index 96faadfc4..d320f4aaa 100644 --- a/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java +++ b/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java @@ -22,7 +22,8 @@ public class InternalBusinessFileService { "CONTAINER_CLEARANCE_DOCUMENT", "LOADING_CUSTOMS_DOCUMENT", "CONTAINER_DELIVERY_ORDER", - "CONTAINER_PICKUP_ORDER"); + "CONTAINER_PICKUP_ORDER", + "OVERSEAS_UNLOAD_PHOTO"); @Resource private FileObjectMapper files; @Resource private BusinessFileStorage storage; diff --git a/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java b/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java index 44fd69118..7b8c7bcd6 100644 --- a/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java +++ b/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java @@ -40,6 +40,8 @@ class InternalBusinessFileServiceTest { when(storage.storeLoading(eq(0L), anyLong(), same(file))).thenAnswer(invocation -> new BusinessFileStorage.Stored("business-files/0/" + invocation.getArgument(1) + "/customs.pdf", "application/pdf", file.getSize(), "b".repeat(64), "customs.pdf")); + when(storage.storeLoading(11L, 51L, file)).thenReturn(new BusinessFileStorage.Stored( + "business-files/11/51/unload.jpg", "image/jpeg", file.getSize(), "c".repeat(64), "unload.jpg")); when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> { ((FileObjectEntity) invocation.getArgument(0)).setId(81L); return 1; }); @@ -48,6 +50,7 @@ class InternalBusinessFileServiceTest { var customs = service.upload(0L, "LOADING_CUSTOMS_DOCUMENT", "51", 7L, file); var deliveryOrder = service.upload(0L, "CONTAINER_DELIVERY_ORDER", "41", 7L, file); var pickupOrder = service.upload(0L, "CONTAINER_PICKUP_ORDER", "41", 7L, file); + var unloadPhoto = service.upload(11L, "OVERSEAS_UNLOAD_PHOTO", "51", 7L, file); assertThat(clearance).extracting(InternalBusinessFileService.Metadata::tenantId, InternalBusinessFileService.Metadata::businessType, @@ -57,8 +60,13 @@ class InternalBusinessFileServiceTest { InternalBusinessFileService.Metadata::businessId).containsExactly(0L, "LOADING_CUSTOMS_DOCUMENT", "51"); assertThat(deliveryOrder.businessType()).isEqualTo("CONTAINER_DELIVERY_ORDER"); assertThat(pickupOrder.businessType()).isEqualTo("CONTAINER_PICKUP_ORDER"); + assertThat(unloadPhoto).extracting(InternalBusinessFileService.Metadata::tenantId, + InternalBusinessFileService.Metadata::businessType, + InternalBusinessFileService.Metadata::businessId) + .containsExactly(11L, "OVERSEAS_UNLOAD_PHOTO", "51"); verify(storage, times(3)).storeLoading(0L, 41L, file); verify(storage).storeLoading(0L, 51L, file); + verify(storage).storeLoading(11L, 51L, file); } @Test void rejectsNegativeTenantAndInvalidOwnershipInputs() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/OverseasContainerEventConsumerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/OverseasContainerEventConsumerTest.java index 390c16439..772830e94 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/OverseasContainerEventConsumerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/OverseasContainerEventConsumerTest.java @@ -55,7 +55,7 @@ class OverseasContainerEventConsumerTest { eq(LocalDateTime.parse("2026-08-22T02:30:00")), isNull(), eq(7L)); ArgumentCaptor captor = ArgumentCaptor.forClass(ContainerEventInboxEntity.class); verify(inbox).insert(captor.capture()); - assertThat(captor.getValue().getAggregateId()).isEqualTo("1001"); + assertThat(captor.getValue().getAggregateId()).isEqualTo("MSCU1234567"); assertThat(captor.getValue().getStatus()).isEqualTo("PROCESSED"); verify(inbox).updateById(captor.getValue()); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegHealthController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegHealthController.java index abb4a10cb..0e8e98834 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegHealthController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegHealthController.java @@ -13,15 +13,15 @@ import java.util.Map; * 尾程预留服务状态接口,用于验证尾程服务边界及基础可用性。 */ @Tag( - name = "Last-leg service status / 尾程服务状态", - description = "Last-leg reserved service availability API. / 尾程预留服务的可用性接口。" + name = "尾程服务状态", + description = "尾程服务可用性接口" ) @RestController @RequestMapping("/api/platform/v1/last-leg") public class LastlegHealthController { @Operation( - summary = "Get service status / 查询服务状态", - description = "Returns the logical status of the reserved lastleg-service boundary. / 返回预留 lastleg-service 边界的逻辑运行状态。" + summary = "查询服务状态", + description = "返回尾程服务边界的逻辑运行状态" ) @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java index d3373c2db..5543eae2e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java @@ -32,14 +32,15 @@ import org.springframework.web.server.ResponseStatusException; import java.nio.charset.StandardCharsets; @RestController -@Tag(name = "Tail-leg orders") +@Tag(name = "尾程订单") public class LastlegOrderController { @Resource private LoadingCompletedConsumer consumer; @Resource private LastlegOrderService orderService; @Resource private InternalServiceTokenGuard tokenGuard; @PostMapping("/internal/lastleg/events") - @Operation(summary = "Receive a first-leg loading-completed event") + /** 接收头程装柜完成事件并创建尾程订单。 */ + @Operation(summary = "接收头程装柜完成事件") public ApiResponse receive( @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, @RequestBody EventEnvelope event) { @@ -48,7 +49,8 @@ public class LastlegOrderController { } @GetMapping("/api/platform/v1/last-leg/orders") - @Operation(summary = "Page tail-leg order rows") + /** 分页查询尾程订单列表。 */ + @Operation(summary = "分页查询尾程订单") public ApiResponse> list( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -57,7 +59,8 @@ public class LastlegOrderController { } @GetMapping("/api/platform/v1/last-leg/orders/{id}") - @Operation(summary = "Get one inbound-code-level tail-leg order") + /** 查询指定入仓单维度的尾程订单详情。 */ + @Operation(summary = "查询尾程订单详情") public ApiResponse detail( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -66,7 +69,8 @@ public class LastlegOrderController { } @GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv") - @Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV") + /** 按筛选条件导出尾程订单 CSV 文件。 */ + @Operation(summary = "导出尾程订单") public ResponseEntity export( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -79,7 +83,8 @@ public class LastlegOrderController { } @PutMapping("/api/platform/v1/last-leg/orders/{id}/destination") - @Operation(summary = "Transfer one order to a destination warehouse") + /** 将单个尾程订单转移到目标仓库。 */ + @Operation(summary = "单个订单转移仓库") public ApiResponse transferOne( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -89,7 +94,8 @@ public class LastlegOrderController { } @PutMapping("/api/platform/v1/last-leg/orders/destination") - @Operation(summary = "Transfer multiple orders to a destination warehouse") + /** 将多个尾程订单批量转移到目标仓库。 */ + @Operation(summary = "批量订单转移仓库") public ApiResponse transferBatch( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java index 49088ef0a..5425714e7 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java @@ -17,12 +17,13 @@ import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/platform/v1/last-leg") -@Tag(name = "Outbound and delivery") +@Tag(name = "出库与派送") public class DeliveryTransferController { @Resource private DeliveryTransferService service; @PostMapping("/dispatches/{dispatchNo}/transfer-delivery") - @Operation(summary = "Create one pending outbound order and one pending delivery order") + /** 创建一个待处理出库单和一个待派送单。 */ + @Operation(summary = "创建出库与派送单") public ApiResponse transfer( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable String dispatchNo, @RequestParam(required = false) String customerId, @@ -30,28 +31,36 @@ public class DeliveryTransferController { return ApiResponse.success(service.transfer(scope(user, customerId), dispatchNo, operator(user), request)); } + /** 分页查询出库单。 */ @GetMapping("/outbound-orders") + @Operation(summary = "分页查询出库单") public ApiResponse> outboundPage( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @Valid DeliveryRequests.Query query) { return ApiResponse.success(service.outboundPage(scope(user, customerId), query)); } + /** 查询出库单详情及托盘信息。 */ @GetMapping("/outbound-orders/{outboundNo}") + @Operation(summary = "查询出库单详情") public ApiResponse outboundDetail( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @PathVariable String outboundNo, @Valid DeliveryRequests.PalletQuery query) { return ApiResponse.success(service.outboundDetail(scope(user, customerId), outboundNo, query)); } + /** 分页查询派送单。 */ @GetMapping("/delivery-orders") + @Operation(summary = "分页查询派送单") public ApiResponse> deliveryPage( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @Valid DeliveryRequests.Query query) { return ApiResponse.success(service.deliveryPage(scope(user, customerId), query)); } + /** 查询派送单详情及托盘信息。 */ @GetMapping("/delivery-orders/{deliveryNo}") + @Operation(summary = "查询派送单详情") public ApiResponse deliveryDetail( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @PathVariable String deliveryNo, @Valid DeliveryRequests.PalletQuery query) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java index 9cf6b7f44..dad601018 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java @@ -32,13 +32,14 @@ import java.util.List; @RestController @RequestMapping("/api/platform/v1/last-leg/dispatches") -@Tag(name = "Tail-leg vehicle dispatch") +@Tag(name = "尾程派车") public class DispatchController { @Resource private DispatchService service; @Resource private DispatchPdaWorkflowService pdaWorkflow; @GetMapping("/candidates") - @Operation(summary = "List unlocked planned or received pallets for intelligent pre-dispatch") + /** 查询可用于智能预派车的未锁定托盘。 */ + @Operation(summary = "查询可派车托盘") public ApiResponse> candidates( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -49,7 +50,8 @@ public class DispatchController { } @PostMapping("/pre-arrange") - @Operation(summary = "Create a pending-departure dispatch and lock all selected pallets") + /** 创建待出发派车单并锁定选中的托盘。 */ + @Operation(summary = "创建预派车单") public ApiResponse preArrange( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -60,7 +62,8 @@ public class DispatchController { } @GetMapping - @Operation(summary = "Page dispatch orders") + /** 分页查询派车单。 */ + @Operation(summary = "分页查询派车单") public ApiResponse> page( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -69,7 +72,8 @@ public class DispatchController { } @GetMapping("/{dispatchNo}") - @Operation(summary = "Get dispatch details and all pallet destinations") + /** 查询派车单详情及托盘目的地。 */ + @Operation(summary = "查询派车单详情") public ApiResponse detail( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -78,7 +82,8 @@ public class DispatchController { } @PutMapping("/{dispatchNo}") - @Operation(summary = "Edit vehicle, driver, appointment, route and departure data") + /** 编辑车辆、司机、预约、路线和出发信息。 */ + @Operation(summary = "编辑派车单") public ApiResponse update( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -88,7 +93,8 @@ public class DispatchController { } @DeleteMapping("/{dispatchNo}") - @Operation(summary = "Cancel a pending dispatch and release its pallet locks") + /** 取消待处理派车单并释放托盘锁定。 */ + @Operation(summary = "取消派车单") public ApiResponse cancel( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -98,7 +104,8 @@ public class DispatchController { } @PostMapping("/{dispatchNo}/depart") - @Operation(summary = "Ship all PDA-picked pallets and mark the dispatch departed") + /** 出库 PDA 已拣货托盘并将派车单标记为已出发。 */ + @Operation(summary = "PDA 出库并完成派车") public ApiResponse depart( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java index 0ae6357f7..390c984f7 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; import java.time.LocalDateTime; -@Schema(description = "Tail-leg order row at inbound-code granularity") +@Schema(description = "按入仓单编码粒度展示的尾程订单列表行") public record LastlegOrderSummary( Long id, String orderNo, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java index 3e9cb5d66..c5ac307ac 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java @@ -20,20 +20,22 @@ import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/platform/v1/last-leg/inbound-plans") -@Tag(name = "Tail-leg inbound plans") +@Tag(name = "尾程入仓计划") public class InboundPlanController { @Resource private InboundPlanService service; @Resource private InboundFbaAppointmentService appointmentService; @PostMapping("/convert") - @Operation(summary = "Convert one inbound order to one inbound plan") + /** 将入仓单转换为入仓计划。 */ + @Operation(summary = "入仓单转入仓计划") public ApiResponse convert(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ConvertInboundPlanRequest request) { return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId())); } @GetMapping("/{planNo}") - @Operation(summary = "Get an inbound plan with lines, pallets and allocations") + /** 查询入仓计划及其明细、托盘和分配信息。 */ + @Operation(summary = "查询入仓计划详情") public ApiResponse detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo, @RequestParam(required = false) String customerId) { @@ -41,7 +43,8 @@ public class InboundPlanController { } @PostMapping("/{planNo}/pallets/supplement") - @Operation(summary = "Add one printable supplemental pallet for a plan destination warehouse") + /** 为计划目标仓新增一个可打印的补充托盘。 */ + @Operation(summary = "新增补充托盘") public ApiResponse supplementPallet(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo, @Valid @RequestBody SupplementPalletRequest request) { @@ -49,7 +52,8 @@ public class InboundPlanController { } @PostMapping("/fba-appointments/batch") - @Operation(summary = "Create one FBA appointment for selected inbound lines") + /** 为选中的入仓明细创建 FBA 预约。 */ + @Operation(summary = "创建 FBA 预约") public ApiResponse batchFbaAppointment( @AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody BatchFbaAppointmentRequest request) { @@ -59,7 +63,8 @@ public class InboundPlanController { } @GetMapping("/fba-appointments/{appointmentNo}") - @Operation(summary = "Get an FBA appointment and its selected inbound lines") + /** 查询 FBA 预约及其选中的入仓明细。 */ + @Operation(summary = "查询 FBA 预约详情") public ApiResponse fbaAppointmentDetail( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable String appointmentNo, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java index 5a502dab5..a4a95a21c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java @@ -39,10 +39,11 @@ public class InboundUnloadCompletedOutboxService { throw new IllegalStateException("Inbound plan has no source container booking ID"); } Instant photoUploadedAt = task.getUnloadPhotoUploadedAt().toInstant(ZoneOffset.UTC); - var payload = new OverseasContainerUnloadedV1(sourceOrder.getSourceContainerBookingId(), - plan.getContainerNo(), photoUploadedAt, photoUploadedAt, null); + var payload = new OverseasContainerUnloadedV1(plan.getContainerNo(), photoUploadedAt, + photoUploadedAt, null); JsonNode payloadNode = objectMapper.valueToTree(payload); - long aggregateVersion = Math.max(1, task.getVersion() == null ? 1 : task.getVersion()); + // Multiple inbound plans can share one container; upload epoch keeps their container events ordered. + long aggregateVersion = photoUploadedAt.toEpochMilli(); String eventKey = EventTypes.OVERSEAS_CONTAINER_UNLOADED + ':' + task.getId(); String traceId = EventIdentifiers.newUuid(); EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(), EventTypes.OVERSEAS_CONTAINER_UNLOADED, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java index 71d44c87b..4ae6bc836 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java @@ -19,14 +19,15 @@ import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/platform/v1/last-leg/inventory") -@Tag(name = "Tail-leg inbound-order package inventory commands") +@Tag(name = "尾程入仓单库存操作") public class InventoryCommandController { public static final String MANAGE_PERMISSION = "lastleg:inventory:manage"; public static final String ADJUST_PERMISSION = "lastleg:inventory:adjust"; @Resource private InventoryCommandService service; @PostMapping("/hold") - @Operation(summary = "Move available inbound-order packages to hold") + /** 将可用入仓单箱数转为冻结。 */ + @Operation(summary = "冻结入仓单库存") public ApiResponse hold(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { requirePermission(user, MANAGE_PERMISSION); @@ -34,7 +35,8 @@ public class InventoryCommandController { } @PostMapping("/release") - @Operation(summary = "Release held inbound-order packages") + /** 释放已冻结的入仓单箱数。 */ + @Operation(summary = "释放入仓单库存") public ApiResponse release(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { requirePermission(user, MANAGE_PERMISSION); @@ -42,7 +44,8 @@ public class InventoryCommandController { } @PostMapping("/allocate") - @Operation(summary = "Allocate available inbound-order packages to an outbound order") + /** 将可用入仓单箱数分配给出库单。 */ + @Operation(summary = "分配出库库存") public ApiResponse allocate(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { requirePermission(user, MANAGE_PERMISSION); @@ -50,7 +53,8 @@ public class InventoryCommandController { } @PostMapping("/unallocate") - @Operation(summary = "Cancel an outbound allocation") + /** 取消出库库存分配。 */ + @Operation(summary = "取消出库分配") public ApiResponse unallocate(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { requirePermission(user, MANAGE_PERMISSION); @@ -58,7 +62,8 @@ public class InventoryCommandController { } @PostMapping("/pick") - @Operation(summary = "Confirm picked inbound-order packages") + /** 确认入仓单箱数已拣货。 */ + @Operation(summary = "确认拣货") public ApiResponse pick(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { requirePermission(user, MANAGE_PERMISSION); @@ -66,7 +71,8 @@ public class InventoryCommandController { } @PostMapping("/ship") - @Operation(summary = "Ship picked inbound-order packages and report whether the pallet is empty") + /** 出库已拣货箱数并返回托盘是否为空。 */ + @Operation(summary = "出库拣货库存") public ApiResponse ship(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { requirePermission(user, MANAGE_PERMISSION); @@ -74,7 +80,8 @@ public class InventoryCommandController { } @PostMapping("/adjust") - @Operation(summary = "Adjust inbound-order package inventory after a physical count") + /** 根据实物盘点结果调整入仓单库存。 */ + @Operation(summary = "盘点调整库存") public ApiResponse adjust(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Adjustment request) { requirePermission(user, ADJUST_PERMISSION); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java index f9b9f759d..3b5bc49e9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java @@ -28,14 +28,15 @@ import java.nio.charset.StandardCharsets; import java.util.List; @RestController -@Tag(name = "Inventory queries") +@Tag(name = "库存查询") public class InventoryQueryController { private static final String BASE_PATH = "/api/platform/v1/last-leg/inventory"; @Resource private InventoryQueryService service; @GetMapping(BASE_PATH + "/summary") - @Operation(summary = "Get inbound-order package inventory totals") + /** 查询入仓单库存汇总。 */ + @Operation(summary = "查询库存汇总") public ApiResponse summary( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -44,7 +45,8 @@ public class InventoryQueryController { } @GetMapping(BASE_PATH + "/balances") - @Operation(summary = "Page inventory by customer, warehouse and inbound order") + /** 按客户、仓库和入仓单分页查询库存。 */ + @Operation(summary = "分页查询库存余额") public ApiResponse> balances( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -53,7 +55,8 @@ public class InventoryQueryController { } @GetMapping(BASE_PATH + "/balances/{balanceId}/positions") - @Operation(summary = "List pallet and batch positions for one inbound-order balance") + /** 查询库存余额对应的托盘和批次位置。 */ + @Operation(summary = "查询库存位置") public ApiResponse> positions( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -63,7 +66,8 @@ public class InventoryQueryController { } @GetMapping(BASE_PATH + "/ledger") - @Operation(summary = "Page append-only inventory movements") + /** 分页查询只增不改的库存流水。 */ + @Operation(summary = "分页查询库存流水") public ApiResponse> ledgers( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -72,7 +76,8 @@ public class InventoryQueryController { } @GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv") - @Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV") + /** 导出筛选后的入仓单库存 CSV 文件。 */ + @Operation(summary = "导出库存余额") public ResponseEntity exportBalances( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @@ -81,7 +86,8 @@ public class InventoryQueryController { } @GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv") - @Operation(summary = "Export filtered inventory movements as UTF-8 CSV") + /** 导出筛选后的库存流水 CSV 文件。 */ + @Operation(summary = "导出库存流水") public ResponseEntity exportLedgers( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxServiceTest.java new file mode 100644 index 000000000..3720afd97 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxServiceTest.java @@ -0,0 +1,71 @@ +package com.baiyue.lastleg.inboundplan.pda.service; + +import com.baiyue.common.event.EventEnvelope; +import com.baiyue.common.event.EventTypes; +import com.baiyue.common.event.RegionCode; +import com.baiyue.lastleg.entity.LastlegEventOutbox; +import com.baiyue.lastleg.entity.LastlegOrder; +import com.baiyue.lastleg.inboundplan.entity.InboundPlan; +import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask; +import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper; +import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class InboundUnloadCompletedOutboxServiceTest { + @Mock LastlegEventOutboxMapper outbox; + @Mock LastlegOrderMapper orders; + private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + private InboundUnloadCompletedOutboxService service; + + @BeforeEach + void setUp() { + service = new InboundUnloadCompletedOutboxService(); + ReflectionTestUtils.setField(service, "mapper", outbox); + ReflectionTestUtils.setField(service, "orders", orders); + ReflectionTestUtils.setField(service, "objectMapper", objectMapper); + } + + @Test + void writesAValidOverseasToCnEnvelopeUsingTheSourceBookingId() throws Exception { + InboundPlan plan = new InboundPlan(); plan.setSourceOrderId(11L); plan.setContainerNo("MSCU1"); + InboundPdaTask task = new InboundPdaTask(); task.setId(22L); task.setCustomerId("100"); + task.setUnloadPhotoUploadedAt(LocalDateTime.of(2026, 8, 20, 10, 0)); task.setVersion(3); + LastlegOrder order = new LastlegOrder(); order.setSourceContainerBookingId("9001"); + when(orders.selectById(11L)).thenReturn(order); + + service.publish(plan, task, List.of(101L)); + + ArgumentCaptor saved = ArgumentCaptor.forClass(LastlegEventOutbox.class); + verify(outbox).insert(saved.capture()); + EventEnvelope envelope = objectMapper.readValue(saved.getValue().getPayloadJson(), EventEnvelope.class); + assertEquals(EventTypes.OVERSEAS_CONTAINER_UNLOADED, envelope.eventType()); + assertEquals(RegionCode.OVERSEAS, envelope.sourceRegion()); + assertEquals(RegionCode.CN, envelope.targetRegion()); + assertEquals("9001", envelope.aggregateId()); + assertEquals("MSCU1", envelope.payload().path("containerNo").asText()); + } + + @Test + void rejectsPlansWhoseSourceOrderLostTheContainerBookingId() { + InboundPlan plan = new InboundPlan(); plan.setSourceOrderId(11L); + InboundPdaTask task = new InboundPdaTask(); task.setId(22L); task.setCustomerId("100"); + when(orders.selectById(11L)).thenReturn(new LastlegOrder()); + assertThrows(IllegalStateException.class, () -> service.publish(plan, task, List.of(101L))); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java index 4f3142a49..cab178bdb 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java @@ -4,7 +4,7 @@ import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com class LoadingCompletedConsumerTest { @Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service; @BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); } - @Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); } + @Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"book-1".equals(o.getSourceContainerBookingId()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); } @Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); } @Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); } @Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }