From a4189a00746169aa61b5e130f625be307856e3b1 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Mon, 31 Aug 2026 14:39: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 --- deploy/sql/海外区域统一增量-MySQL8.sql | 16 ++++ .../controller/LastlegOrderController.java | 17 +++- .../pda/dto/DispatchPdaResponses.java | 3 +- .../service/DispatchPdaWorkflowService.java | 9 ++- .../lastleg/dto/LastlegOrderRequests.java | 14 +++- .../lastleg/dto/LastlegOrderSummary.java | 3 +- .../baiyue/lastleg/entity/LastlegOrder.java | 1 + .../dto/InboundPlanListResponse.java | 5 +- .../inboundplan/dto/InboundPlanResponse.java | 13 +++- .../inboundplan/entity/InboundPlan.java | 3 + .../mapper/InboundPlanOrderMapper.java | 9 +-- .../mapper/InboundPlanQueryMapper.java | 5 +- .../service/InboundPlanService.java | 18 ++++- .../controller/InventoryQueryController.java | 9 ++- .../query/service/InventoryQueryService.java | 69 ++++++++++++---- .../lastleg/mapper/LastlegOrderMapper.java | 22 +++++- .../lastleg/service/LastlegOrderService.java | 78 ++++++++++++++++--- .../src/main/resources/db/lastleg.sql | 5 ++ .../LastlegOrderControllerTest.java | 28 +++++++ .../DispatchPdaWorkflowServiceTest.java | 8 ++ .../InboundPlanOrderMapperContractTest.java | 21 +++++ .../InboundPlanQueryMapperContractTest.java | 22 ++++++ .../service/InboundPlanServiceTest.java | 16 ++-- .../service/LastlegOrderServiceTest.java | 8 +- scripts/db/incremental.sql | 7 ++ 25 files changed, 345 insertions(+), 64 deletions(-) create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapperContractTest.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index d55f1a6dd..22681b015 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -1055,6 +1055,22 @@ CREATE TABLE IF NOT EXISTS ll_order_operation_log ( ) ENGINE=InnoDB COMMENT='尾程订单操作记录'; USE baiyue_lastleg; +SET @ddl = (SELECT IF(EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan' AND column_name='version' +), 'SELECT 1', 'ALTER TABLE ll_inbound_plan ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT ''入库计划乐观锁版本'' AFTER updated_at')); +PREPARE ll_inbound_plan_version_ddl FROM @ddl; +EXECUTE ll_inbound_plan_version_ddl; +DEALLOCATE PREPARE ll_inbound_plan_version_ddl; + +SET @ddl = (SELECT IF(EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='pallet_merge_batch_id' +), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN pallet_merge_batch_id VARCHAR(64) NULL COMMENT ''平台预标记合托分组号'' AFTER pallet_merge_flag')); +PREPARE ll_order_merge_batch_ddl FROM @ddl; +EXECUTE ll_order_merge_batch_ddl; +DEALLOCATE PREPARE ll_order_merge_batch_ddl; + CREATE TABLE IF NOT EXISTS ll_security_event_inbox ( event_id VARCHAR(64) NOT NULL COMMENT '跨区安全事件ID', event_type VARCHAR(96) NOT NULL COMMENT 'IAM安全事件类型', payload_hash CHAR(64) NOT NULL COMMENT '规范化载荷SHA-256哈希', payload_json JSON NOT NULL COMMENT '冻结事件载荷JSON', 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 d9762c336..201e61a28 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,7 +32,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import org.springframework.web.server.ResponseStatusException; import java.nio.charset.StandardCharsets; @@ -119,19 +118,29 @@ public class LastlegOrderController { return ApiResponse.success(null); } + @PutMapping("/api/platform/v1/last-leg/orders/pallet-merge-required/batch") + @PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')") + @Operation(summary = "Mark a selected tail-leg order group for PDA pallet merge") + public ApiResponse setPalletMergeBatch( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam(required = false) String customerId, + @Valid @RequestBody LastlegOrderRequests.SetPalletMergeBatch request) { + orderService.setPalletMergeBatch(platformScope(user, customerId), request, operatorName(user)); + return ApiResponse.success(null); + } + @GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv") @PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EXPORT + "')") @Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EXPORT) - public ResponseEntity export( + public ResponseEntity export( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @Valid LastlegOrderRequests.Query query) { byte[] body = orderService.exportCsv(platformScope(user, customerId), query); - StreamingResponseBody stream = output -> { output.write(body); output.flush(); }; return ResponseEntity.ok() .contentType(new MediaType("text", "csv", StandardCharsets.UTF_8)) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=lastleg-orders.csv") - .body(stream); + .body(body); } @PutMapping("/api/platform/v1/last-leg/orders/{id}/destination") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java index 638d51be2..e259a89b8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java @@ -7,7 +7,8 @@ public final class DispatchPdaResponses { public record Task(Long id, String taskNo, String dispatchNo, String vehiclePlate, String warehouseId, String status, List items) {} public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode, - String destinationWarehouseName, String fbaWarehouseCode, int scheduledPackages, + String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel, + int scheduledPackages, long pickedPackages, List positions) {} public record Position(Long id, Long balanceId, long availablePackages, long allocatedPackages, long pickedPackages) {} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowService.java index 19b5979d4..537ca0f97 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowService.java @@ -225,7 +225,8 @@ public class DispatchPdaWorkflowService { List palletPositions = item.getWarehousePalletId() == null ? List.of() : positions.listPallet(customerId, item.getWarehousePalletId()); values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(), - item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), item.getPackageCount(), + item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), + warehouseRouteLabel(item.getFbaWarehouseCode(), item.getDestinationWarehouseCode()), item.getPackageCount(), picks.totalItem(customerId, task.getId(), item.getId()), palletPositions.stream().map(p -> new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()), n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList())); @@ -268,6 +269,12 @@ public class DispatchPdaWorkflowService { private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException(e); } } private static long n(Long value) { return value == null ? 0 : value; } private static long n(Integer value) { return value == null ? 0 : value; } + static String warehouseRouteLabel(String originalFbaWarehouseCode, String destinationWarehouseCode) { + String original = trim(originalFbaWarehouseCode); + String destination = trim(destinationWarehouseCode); + return original != null && destination != null && !original.equalsIgnoreCase(destination) + ? original + " -> " + destination : null; + } private static String trim(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private static String required(String value, String message) { String result = trim(value); if (result == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); return result; } private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java index 122ae8520..1b3c2da56 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java @@ -18,8 +18,8 @@ public final class LastlegOrderRequests { } public record Query( - @Min(1) long page, - @Min(1) @Max(200) long size, + @Min(1) Long page, + @Min(1) @Max(200) Long size, String orderNo, String orderType, String inboundCode, @@ -35,8 +35,8 @@ public final class LastlegOrderRequests { LocalDateTime createdTo ) { public Query { - if (page == 0) page = 1; - if (size == 0) size = 20; + if (page == null || page == 0) page = 1L; + if (size == null || size == 0) size = 20L; } } @@ -79,4 +79,10 @@ public final class LastlegOrderRequests { @NotNull Boolean palletMergeRequired ) { } + + public record SetPalletMergeBatch( + @NotEmpty @Size(max = 200) List orderIds, + @NotNull Boolean palletMergeRequired + ) { + } } 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 d0a5ca5c7..6384a8b69 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 @@ -42,7 +42,8 @@ public record LastlegOrderSummary( String relabelStatus, String palletMergeFlag, String palletMergeBatchId, - List palletMergeOrderNos, + @Schema(description = "同一合托组内其他来源入仓单的入仓码") + List palletMergeInboundNos, String currentStatus, String subOrderStatus, String privateRemark, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java index 89234d307..4f48bb76c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java @@ -52,6 +52,7 @@ public class LastlegOrder { private String relabelOrderNo; private String relabelStatus; private String palletMergeFlag; + private String palletMergeBatchId; private String currentStatus; private String subOrderStatus; private String privateRemark; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java index df4947455..d59689a19 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java @@ -26,7 +26,8 @@ public record InboundPlanListResponse( LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt, String dispatchStatus, - LocalDateTime createdAt + LocalDateTime createdAt, + Integer version ) { public InboundPlanListResponse(Long lineId, Long planId, String planNo, String customerId, Long orderId, String inboundCode, String orderType, @@ -39,6 +40,6 @@ public record InboundPlanListResponse( this(lineId, planId, planNo, customerId, orderId, inboundCode, orderType, warehouseId, warehouseCode, warehouseName, fbaWarehouseCode, customerName, packageCount, 0, volumeCbm, palletCount, status, appointmentNo, appointmentStatus, appointmentAt, - expectedLoadingAt, dispatchStatus, createdAt); + expectedLoadingAt, dispatchStatus, createdAt, 0); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java index ffafec563..0fd8cd3e6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java @@ -18,15 +18,24 @@ public record InboundPlanResponse( int pickedPackageCount, List lines, List pallets, - List allocations + List allocations, + int version ) { public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId, String containerNo, String status, int lineCount, int palletCount, BigDecimal totalVolumeCbm, List lines, List pallets, List allocations) { + this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount, + totalVolumeCbm, lines, pallets, allocations, 0); + } + + public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId, + String containerNo, String status, int lineCount, int palletCount, + BigDecimal totalVolumeCbm, List lines, List pallets, + List allocations, int version) { this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount, totalVolumeCbm, lines.stream().mapToInt(Line::requiredPackageCount).sum(), - allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations); + allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations, version); } public record Line(Long id, Long orderId, String inboundCode, String fbaWarehouseCode, String destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlan.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlan.java index 79ffac343..51c30b470 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlan.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlan.java @@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.Version; import lombok.Data; import java.math.BigDecimal; @@ -24,4 +25,6 @@ public class InboundPlan { private BigDecimal totalVolumeCbm; private LocalDateTime createdAt; private LocalDateTime updatedAt; + @Version + private Integer version; } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java index 44952b38c..88cdade9b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java @@ -23,12 +23,9 @@ public interface InboundPlanOrderMapper { "COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,''))) AS destinationWarehouseId," + "COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,''))) AS destinationWarehouseCode," + "COALESCE(NULLIF(o.transfer_warehouse_name,''),MIN(NULLIF(d.destination_warehouse_name,''))) AS destinationWarehouseName," + - "COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) AS destinationCount," + - "SUM(CASE WHEN d.id IS NOT NULL " + - "AND COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL " + - "AND COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#') " + - "<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " + - "AS destinationMismatchCount " + + "CASE WHEN COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL THEN 1 " + + "ELSE COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) END AS destinationCount," + + "0 AS destinationMismatchCount " + "FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " + "WHERE o.customer_id=#{customerId} AND o.id=#{orderId} " + "GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.pallet_count,o.fba_warehouse_code," + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java index 97a4476f7..aea91cc5b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java @@ -45,8 +45,9 @@ public interface InboundPlanQueryMapper { "l.planned_pallet_count palletCount,p.status status,a.appointment_no appointmentNo," + "a.appointment_status appointmentStatus,a.appointment_at appointmentAt," + "a.expected_loading_at expectedLoadingAt," + - "CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)" + - " THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt" + + "CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.order_id=l.order_id" + + " AND di.customer_id=#{customerId})" + + " THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt,p.version version" + FROM_AND_FILTER + " ORDER BY p.created_at DESC,l.id DESC LIMIT #{limit} OFFSET #{offset}") List rows(@Param("customerId") String customerId, @Param("q") InboundPlanRequests.Query query, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java index 390204228..3eae2cef2 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java @@ -227,9 +227,22 @@ public class InboundPlanService { if (plan == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found"); if (!java.util.Set.of("PENDING_ASSIGNMENT", "ASSIGNED").contains(plan.getStatus())) throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound plan has started and cannot be edited"); + int currentVersion = plan.getVersion() == null ? 0 : plan.getVersion(); + if (request.version() != currentVersion) + throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound plan version is stale; reload before editing"); plan.setOverseasWarehouseId(request.overseasWarehouseId()); plan.setUpdatedAt(LocalDateTime.now()); planMapper.updateById(plan); + // Keep the denormalized destination snapshot used by list queries in sync with the plan. + List lines = lineMapper.selectList(new LambdaQueryWrapper() + .eq(InboundPlanLine::getPlanId, plan.getId()) + .eq(InboundPlanLine::getCustomerId, plan.getCustomerId())); + for (InboundPlanLine line : lines) { + line.setDestinationWarehouseId(String.valueOf(request.overseasWarehouseId())); + if (trimToNull(request.warehouseCode()) != null) line.setDestinationWarehouseCode(request.warehouseCode().trim()); + if (trimToNull(request.warehouseName()) != null) line.setDestinationWarehouseName(request.warehouseName().trim()); + lineMapper.updateById(line); + } return detail(customerId, plan); } @@ -342,7 +355,7 @@ public class InboundPlanService { throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound code " + order.inboundCode() + " is not ready for an inbound plan"); } - if (order.destinationCount() != 1 || order.destinationMismatchCount() != 0 || warehouseKey(order) == null) { + if (order.destinationCount() != 1 || warehouseKey(order) == null) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Inbound code " + order.inboundCode() + " must have one destination warehouse consistent with all cargo details"); @@ -388,7 +401,8 @@ public class InboundPlanService { pallet.getActualPackageCount())).toList(), allocations.stream().map(item -> new InboundPlanResponse.Allocation(item.getLineId(), item.getPalletId(), item.getAllocatedVolumeCbm(), item.getPlannedPackageCount(), item.getCountedPackageCount(), - item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList()); + item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList(), + plan.getVersion() == null ? 0 : plan.getVersion()); } private BigDecimal volume(SourceOrderSnapshot order) { 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 eeea51bf8..efb7abbbe 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 @@ -85,7 +85,8 @@ public class InventoryQueryController { @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @Valid InventoryQueryRequests.BalanceQuery query) { - return csv(service.exportBalances(platformScope(user, customerId), query), "inventory-balances.csv"); + String scope = platformScope(user, customerId); + return csv(output -> service.writeBalancesCsv(scope, query, output), "inventory-balances.csv"); } @GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv") @@ -95,11 +96,11 @@ public class InventoryQueryController { @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @Valid InventoryQueryRequests.LedgerQuery query) { - return csv(service.exportLedgers(platformScope(user, customerId), query), "inventory-ledger.csv"); + String scope = platformScope(user, customerId); + return csv(output -> service.writeLedgersCsv(scope, query, output), "inventory-ledger.csv"); } - private static ResponseEntity csv(byte[] body, String filename) { - StreamingResponseBody stream = output -> { output.write(body); output.flush(); }; + private static ResponseEntity csv(StreamingResponseBody stream, String filename) { return ResponseEntity.ok() .contentType(new MediaType("text", "csv", StandardCharsets.UTF_8)) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java index eb6bcab9a..c1252cc6e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java @@ -17,13 +17,14 @@ import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.List; import java.util.Set; @Service public class InventoryQueryService { - private static final int EXPORT_LIMIT = 10_000; private static final Set BALANCE_STATUSES = Set.of( "AVAILABLE", "HOLD", "ALLOCATED", "PICKED", "IN_TRANSIT", "RECEIVING", "PROCUREMENT"); private static final Set POSITION_STATUSES = Set.of( @@ -79,34 +80,74 @@ public class InventoryQueryService { } public byte[] exportBalances(String customerId, InventoryQueryRequests.BalanceQuery query) { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + validateStatus(query.inventoryStatus(), BALANCE_STATUSES); + output.write(("\uFEFFcustomerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode,destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages,availablePackages,holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages\r\n").getBytes(StandardCharsets.UTF_8)); + for (InventoryBalance value : mapper.balances(customerId, query, 0, 10_000)) { + InventoryBalanceView v = balanceView(value); + output.write((String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()), csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.skuCode()), csv(v.productNameZh()), csv(v.productNameEn()), csv(v.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()), csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()), csv(v.procurementPackages())) + "\r\n").getBytes(StandardCharsets.UTF_8)); + } + return output.toByteArray(); + } catch (IOException e) { throw new IllegalStateException("Unable to export inventory balances", e); } + } + + public void writeBalancesCsv(String customerId, InventoryQueryRequests.BalanceQuery query, OutputStream output) + throws IOException { validateStatus(query.inventoryStatus(), BALANCE_STATUSES); - List rows = new ArrayList<>(); - rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," + + output.write(("\uFEFFcustomerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," + "destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages,availablePackages," + - "holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages"); - mapper.balances(customerId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add( + "holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages\r\n").getBytes(StandardCharsets.UTF_8)); + final int batchSize = 1000; + long offset = 0; + while (true) { + List batch = mapper.balances(customerId, query, offset, batchSize).stream().map(this::balanceView).toList(); + if (batch.isEmpty()) break; + for (InventoryBalanceView v : batch) output.write(( String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()), csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.skuCode()), csv(v.productNameZh()), csv(v.productNameEn()), csv(v.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()), csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()), - csv(v.procurementPackages())))); - return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8); + csv(v.procurementPackages())) + "\r\n").getBytes(StandardCharsets.UTF_8)); + output.flush(); + if (batch.size() < batchSize) break; + offset += batchSize; + } } public byte[] exportLedgers(String customerId, InventoryQueryRequests.LedgerQuery query) { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + validateDateRange(query); + output.write(("\uFEFFoccurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo,inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo,beforePackages,deltaPackages,afterPackages,operatorId,remark\r\n").getBytes(StandardCharsets.UTF_8)); + for (InventoryLedger value : mapper.ledgers(customerId, query, 0, 10_000)) { + InventoryLedgerView v = ledgerView(value); + output.write((String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()), csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()), csv(v.statusFrom()), csv(v.statusTo()), csv(v.beforePackages()), csv(v.deltaPackages()), csv(v.afterPackages()), csv(v.operatorId()), csv(v.remark())) + "\r\n").getBytes(StandardCharsets.UTF_8)); + } + return output.toByteArray(); + } catch (IOException e) { throw new IllegalStateException("Unable to export inventory ledger", e); } + } + + public void writeLedgersCsv(String customerId, InventoryQueryRequests.LedgerQuery query, OutputStream output) + throws IOException { validateDateRange(query); - List rows = new ArrayList<>(); - rows.add("occurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," + + output.write(("\uFEFFoccurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," + "inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo," + - "beforePackages,deltaPackages,afterPackages,operatorId,remark"); - mapper.ledgers(customerId, query, 0, EXPORT_LIMIT).stream().map(this::ledgerView).forEach(v -> rows.add( + "beforePackages,deltaPackages,afterPackages,operatorId,remark\r\n").getBytes(StandardCharsets.UTF_8)); + final int batchSize = 1000; + long offset = 0; + while (true) { + List batch = mapper.ledgers(customerId, query, offset, batchSize).stream().map(this::ledgerView).toList(); + if (batch.isEmpty()) break; + for (InventoryLedgerView v : batch) output.write(( String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()), csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()), csv(v.statusFrom()), csv(v.statusTo()), csv(v.beforePackages()), csv(v.deltaPackages()), - csv(v.afterPackages()), csv(v.operatorId()), csv(v.remark())))); - return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8); + csv(v.afterPackages()), csv(v.operatorId()), csv(v.remark())) + "\r\n").getBytes(StandardCharsets.UTF_8)); + output.flush(); + if (batch.size() < batchSize) break; + offset += batchSize; + } } private InventoryBalanceView balanceView(InventoryBalance value) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java index ddb631144..f0e93104b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java @@ -11,10 +11,18 @@ import java.util.List; @Mapper public interface LastlegOrderMapper extends BaseMapper { - record PalletMergeRelationRow(Long orderId, String relatedOrderNo, String mergeGroupNo) {} + record PalletMergeRelationRow(Long orderId, String relatedInboundNo, String mergeGroupNo) {} - @Select("