提交
This commit is contained in:
@@ -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',
|
||||
|
||||
+13
-4
@@ -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<Void> 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<StreamingResponseBody> export(
|
||||
public ResponseEntity<byte[]> 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")
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ public final class DispatchPdaResponses {
|
||||
public record Task(Long id, String taskNo, String dispatchNo, String vehiclePlate, String warehouseId, String status,
|
||||
List<Item> 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<Position> positions) {}
|
||||
public record Position(Long id, Long balanceId, long availablePackages, long allocatedPackages,
|
||||
long pickedPackages) {}
|
||||
|
||||
+8
-1
@@ -225,7 +225,8 @@ public class DispatchPdaWorkflowService {
|
||||
List<InventoryPosition> 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); }
|
||||
|
||||
@@ -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<Long> orderIds,
|
||||
@NotNull Boolean palletMergeRequired
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ public record LastlegOrderSummary(
|
||||
String relabelStatus,
|
||||
String palletMergeFlag,
|
||||
String palletMergeBatchId,
|
||||
List<String> palletMergeOrderNos,
|
||||
@Schema(description = "同一合托组内其他来源入仓单的入仓码")
|
||||
List<String> palletMergeInboundNos,
|
||||
String currentStatus,
|
||||
String subOrderStatus,
|
||||
String privateRemark,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -18,15 +18,24 @@ public record InboundPlanResponse(
|
||||
int pickedPackageCount,
|
||||
List<Line> lines,
|
||||
List<Pallet> pallets,
|
||||
List<Allocation> allocations
|
||||
List<Allocation> allocations,
|
||||
int version
|
||||
) {
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> 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<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> 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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+3
-6
@@ -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," +
|
||||
|
||||
+3
-2
@@ -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" +
|
||||
" <if test='customerId != null and customerId != \"\"'>AND di.customer_id=#{customerId}</if>)" +
|
||||
" 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}</script>")
|
||||
List<InboundPlanListResponse> rows(@Param("customerId") String customerId,
|
||||
@Param("q") InboundPlanRequests.Query query,
|
||||
|
||||
+16
-2
@@ -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<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.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) {
|
||||
|
||||
+5
-4
@@ -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<StreamingResponseBody> csv(byte[] body, String filename) {
|
||||
StreamingResponseBody stream = output -> { output.write(body); output.flush(); };
|
||||
private static ResponseEntity<StreamingResponseBody> csv(StreamingResponseBody stream, String filename) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||
|
||||
+55
-14
@@ -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<String> BALANCE_STATUSES = Set.of(
|
||||
"AVAILABLE", "HOLD", "ALLOCATED", "PICKED", "IN_TRANSIT", "RECEIVING", "PROCUREMENT");
|
||||
private static final Set<String> 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<String> 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<InventoryBalanceView> 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<String> 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<InventoryLedgerView> 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) {
|
||||
|
||||
@@ -11,10 +11,18 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
record PalletMergeRelationRow(Long orderId, String relatedOrderNo, String mergeGroupNo) {}
|
||||
record PalletMergeRelationRow(Long orderId, String relatedInboundNo, String mergeGroupNo) {}
|
||||
|
||||
@Select("<script>SELECT relation.order_id AS orderId, related.order_no AS relatedOrderNo, " +
|
||||
@Select("<script>SELECT relation.order_id AS orderId," +
|
||||
"COALESCE(NULLIF(related.inbound_code,''),NULLIF(related.inbound_order_no,''),related.source_order_no) AS relatedInboundNo, " +
|
||||
"relation.merge_group_no AS mergeGroupNo FROM (" +
|
||||
"SELECT marked.customer_id,marked.id AS order_id,related.id AS related_order_id," +
|
||||
"marked.pallet_merge_batch_id AS merge_group_no FROM ll_order marked " +
|
||||
"JOIN ll_order related ON related.customer_id=marked.customer_id " +
|
||||
"AND related.pallet_merge_batch_id=marked.pallet_merge_batch_id AND related.id<>marked.id " +
|
||||
"WHERE marked.pallet_merge_batch_id IS NOT NULL AND marked.id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"UNION ALL " +
|
||||
"SELECT customer_id,source_order_id AS order_id,target_order_id AS related_order_id,merge_group_no " +
|
||||
"FROM ll_inbound_pallet_merge WHERE status='CONFIRMED' AND source_order_id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
@@ -59,9 +67,17 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("warehouseName") String warehouseName);
|
||||
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag},updated_at=#{updatedAt} " +
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag}," +
|
||||
"pallet_merge_batch_id=CASE WHEN #{flag} IS NULL THEN NULL ELSE pallet_merge_batch_id END," +
|
||||
"updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id}")
|
||||
int updatePalletMergeFlag(@Param("customerId") String customerId, @Param("id") long id,
|
||||
@Param("flag") String flag,
|
||||
@Param("updatedAt") java.time.LocalDateTime updatedAt);
|
||||
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag},pallet_merge_batch_id=#{batchId},updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id}")
|
||||
int updatePalletMergeBatch(@Param("customerId") String customerId, @Param("id") long id,
|
||||
@Param("flag") String flag, @Param("batchId") String batchId,
|
||||
@Param("updatedAt") java.time.LocalDateTime updatedAt);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -181,6 +184,46 @@ public class LastlegOrderService {
|
||||
operationLogMapper.insert(operation);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setPalletMergeBatch(String customerId, LastlegOrderRequests.SetPalletMergeBatch request,
|
||||
String operatorName) {
|
||||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (ids.isEmpty()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
if (orders.size() != ids.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
|
||||
}
|
||||
if (orders.stream().map(LastlegOrder::getCustomerId).distinct().count() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Pallet merge orders must belong to one customer");
|
||||
}
|
||||
if (orders.stream().map(order -> StringUtils.hasText(order.getTransferWarehouseCode())
|
||||
? order.getTransferWarehouseCode() : order.getFbaWarehouseCode()).filter(Objects::nonNull).distinct().count() > 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Pallet merge orders must have one destination warehouse");
|
||||
}
|
||||
if (orders.stream().anyMatch(order -> "INBOUND_COMPLETED".equals(order.getCurrentStatus())
|
||||
|| PALLET_MERGED.equals(order.getPalletMergeFlag()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Completed pallet merge orders cannot be changed");
|
||||
}
|
||||
String batchId = request.palletMergeRequired()
|
||||
? "PMG-" + java.util.UUID.randomUUID().toString().replace("-", "") : null;
|
||||
String flag = request.palletMergeRequired() ? PALLET_MERGE_REQUIRED : null;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (LastlegOrder order : orders) {
|
||||
if (orderMapper.updatePalletMergeBatch(order.getCustomerId(), order.getId(), flag, batchId, now) != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet merge group was not updated");
|
||||
}
|
||||
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
||||
operation.setId(IdWorker.getId()); operation.setCustomerId(order.getCustomerId());
|
||||
operation.setOrderId(order.getId());
|
||||
operation.setOperationNode(request.palletMergeRequired() ? "批量标记合托" : "取消合托标记");
|
||||
operation.setOperatorName(operatorName);
|
||||
operation.setResult(request.palletMergeRequired() ? "PDA合托分组 " + batchId : "已取消PDA合托分组");
|
||||
operation.setCreatedAt(now); operationLogMapper.insert(operation);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
|
||||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||||
@@ -236,12 +279,25 @@ public class LastlegOrderService {
|
||||
}
|
||||
|
||||
public byte[] exportCsv(String customerId, LastlegOrderRequests.Query query) {
|
||||
List<LastlegOrder> orders = orderMapper.selectList(filter(customerId, query)
|
||||
.orderByDesc(LastlegOrder::getCreatedAt).last("LIMIT 10000"));
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)");
|
||||
for (LastlegOrder order : orders) {
|
||||
lines.add(String.join(",", List.of(
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
writeExportCsv(customerId, query, output);
|
||||
return output.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to export tail-leg orders", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeExportCsv(String customerId, LastlegOrderRequests.Query query, OutputStream output)
|
||||
throws IOException {
|
||||
output.write("\uFEFFID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
final int batchSize = 1000;
|
||||
long page = 1;
|
||||
while (true) {
|
||||
List<LastlegOrder> orders = orderMapper.selectPage(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, batchSize),
|
||||
filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId)).getRecords();
|
||||
if (orders.isEmpty()) break;
|
||||
for (LastlegOrder order : orders) {
|
||||
String line = String.join(",", List.of(
|
||||
csv(order.getId()), csv(order.getOrderNo()), csv(LastlegOrderType.labelOf(order.getOrderType())),
|
||||
csv(order.getInboundCode()),
|
||||
csv(order.getSourceOrderNo()), csv(order.getCustomerName()), csv(order.getOverseasWarehouseName()),
|
||||
@@ -251,9 +307,13 @@ public class LastlegOrderService {
|
||||
csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()),
|
||||
csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getRelabelOrderNo()), csv(order.getRelabelStatus()),
|
||||
csv(order.getPalletMergeFlag()), csv(order.getCurrentStatus()), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()),
|
||||
csv(order.getCustomerRemark()), csv(order.getWarehouseRemark()))));
|
||||
csv(order.getCustomerRemark()), csv(order.getWarehouseRemark()))) + "\r\n";
|
||||
output.write(line.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
output.flush();
|
||||
if (orders.size() < batchSize) break;
|
||||
page++;
|
||||
}
|
||||
return ("\uFEFF" + String.join("\r\n", lines)).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
|
||||
@@ -320,7 +380,7 @@ public class LastlegOrderService {
|
||||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getRelabelOrderNo(), o.getRelabelStatus(),
|
||||
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
|
||||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedOrderNo).filter(Objects::nonNull).distinct().toList(),
|
||||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(),
|
||||
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
|
||||
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ CREATE TABLE IF NOT EXISTS ll_order (
|
||||
relabel_order_no VARCHAR(128) COMMENT '换标单号',
|
||||
relabel_status VARCHAR(32) COMMENT '换标状态',
|
||||
pallet_merge_flag VARCHAR(32) COMMENT '合托标记',
|
||||
pallet_merge_batch_id VARCHAR(64) NULL COMMENT '平台预标记合托分组号',
|
||||
current_status VARCHAR(32) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '当前业务状态:PENDING_ASSIGNMENT待分配、PENDING_INBOUND待入库、INBOUND_COMPLETED入库完成、PENDING_DELIVERY待派送、DELIVERING派送中、DELIVERED派送完成',
|
||||
sub_order_status VARCHAR(255) NULL COMMENT '子单据状态汇总',
|
||||
private_remark VARCHAR(1000) COMMENT '备注(仅自己)',
|
||||
@@ -97,6 +98,9 @@ CREATE TABLE IF NOT EXISTS ll_order (
|
||||
KEY idx_ll_order_list(customer_id,current_status,created_at)
|
||||
) ENGINE=InnoDB COMMENT='尾程订单主表';
|
||||
|
||||
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_order_detail (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单明细主键',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
|
||||
@@ -432,6 +436,7 @@ CREATE TABLE IF NOT EXISTS ll_inbound_plan (
|
||||
total_volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '集装箱总体积(立方米)',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '入库计划乐观锁版本',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ll_inbound_plan_no (plan_no),
|
||||
UNIQUE KEY uk_ll_inbound_plan_source_order (customer_id, source_order_id),
|
||||
|
||||
+28
@@ -18,6 +18,8 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
@@ -50,6 +52,32 @@ class LastlegOrderControllerTest {
|
||||
verify(service).page(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportBuildsCsvBeforeReturningTheResponse() {
|
||||
byte[] csv = "csv".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
when(service.exportCsv("200", query)).thenReturn(csv);
|
||||
|
||||
var response = controller.export(user(0L, "PLATFORM"), "200", query);
|
||||
|
||||
assertArrayEquals(csv, response.getBody());
|
||||
verify(service).exportCsv("200", query);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOperatorCanExportAllCustomersWithoutFilters() {
|
||||
byte[] csv = "all customers".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
LastlegOrderRequests.Query emptyQuery = new LastlegOrderRequests.Query(
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
when(service.exportCsv(null, emptyQuery)).thenReturn(csv);
|
||||
|
||||
var response = controller.export(user(0L, "PLATFORM"), null, emptyQuery);
|
||||
|
||||
assertEquals(Long.valueOf(1), emptyQuery.page());
|
||||
assertEquals(Long.valueOf(20), emptyQuery.size());
|
||||
assertArrayEquals(csv, response.getBody());
|
||||
verify(service).exportCsv(isNull(), eq(emptyQuery));
|
||||
}
|
||||
|
||||
@Test
|
||||
void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() {
|
||||
EventEnvelope event = mock(EventEnvelope.class);
|
||||
|
||||
+8
@@ -16,6 +16,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -25,6 +26,13 @@ class DispatchPdaWorkflowServiceTest {
|
||||
@Mock private DispatchPdaOperationMapper operations;
|
||||
@Mock private OutboundOrderMapper outboundOrders;
|
||||
|
||||
@Test
|
||||
void labelsTransferredPalletsWithOriginalAndCurrentFbaWarehouses() {
|
||||
assertEquals("ONT8 -> ONT", DispatchPdaWorkflowService.warehouseRouteLabel("ONT8", "ONT"));
|
||||
assertNull(DispatchPdaWorkflowService.warehouseRouteLabel("ONT8", "ont8"));
|
||||
assertNull(DispatchPdaWorkflowService.warehouseRouteLabel(null, "ONT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushRequiresVehiclePlateBeforeCreatingPdaTask() {
|
||||
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.inboundplan.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InboundPlanOrderMapperContractTest {
|
||||
@Test
|
||||
void transferWarehouseIsTheEffectivePalletDestination() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("lockOrder", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,'')))"));
|
||||
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,'')))"));
|
||||
assertTrue(sql.contains("THEN 1 ELSE COUNT(DISTINCT"));
|
||||
assertTrue(sql.contains("0 AS destinationMismatchCount"));
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.inboundplan.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InboundPlanQueryMapperContractTest {
|
||||
@Test
|
||||
void platformListReadsVersionAndDoesNotRequireANullDispatchCustomer() throws Exception {
|
||||
Select select = InboundPlanQueryMapper.class
|
||||
.getMethod("rows", String.class,
|
||||
com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests.Query.class,
|
||||
long.class, long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("p.version version"));
|
||||
assertTrue(sql.contains("WHERE di.order_id=l.order_id"));
|
||||
assertTrue(sql.contains("<if test='customerId != null and customerId != \"\"'>AND di.customer_id=#{customerId}</if>"));
|
||||
}
|
||||
}
|
||||
+11
-5
@@ -163,13 +163,19 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTransferWarehouseThatDiffersFromCargoSnapshots() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
void transferWarehouseOverridesOriginalCargoDestination() {
|
||||
SourceOrderSnapshot transferred = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(transferred));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
service.convert("100", 1L);
|
||||
|
||||
ArgumentCaptor<InboundPlanPallet> pallet = ArgumentCaptor.forClass(InboundPlanPallet.class);
|
||||
verify(palletMapper).insert(pallet.capture());
|
||||
assertEquals("WH-B", pallet.getValue().getDestinationWarehouseId());
|
||||
assertEquals("WH-B", pallet.getValue().getDestinationWarehouseCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-4
@@ -194,7 +194,7 @@ class LastlegOrderServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageReturnsConfirmedPalletMergeRelatedOrderNumbers() {
|
||||
void pageReturnsPalletMergeRelatedInboundNumbers() {
|
||||
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IB-1");
|
||||
order.setPalletMergeFlag("MERGED");
|
||||
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
|
||||
@@ -202,15 +202,15 @@ class LastlegOrderServiceTest {
|
||||
resultPage.setTotal(1);
|
||||
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
||||
when(orders.findPalletMergeRelations(List.of(11L))).thenReturn(List.of(
|
||||
new LastlegOrderMapper.PalletMergeRelationRow(11L, "LMO-RELATED-2", "PMG-1"),
|
||||
new LastlegOrderMapper.PalletMergeRelationRow(11L, "LMO-RELATED-3", "PMG-1")));
|
||||
new LastlegOrderMapper.PalletMergeRelationRow(11L, "INBOUND-2", "PMG-1"),
|
||||
new LastlegOrderMapper.PalletMergeRelationRow(11L, "INBOUND-3", "PMG-1")));
|
||||
var query = new LastlegOrderRequests.Query(1, 10, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
assertEquals("PMG-1", result.records().get(0).palletMergeBatchId());
|
||||
assertEquals(List.of("LMO-RELATED-2", "LMO-RELATED-3"), result.records().get(0).palletMergeOrderNos());
|
||||
assertEquals(List.of("INBOUND-2", "INBOUND-3"), result.records().get(0).palletMergeInboundNos());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -239,6 +239,9 @@ CREATE TABLE IF NOT EXISTS ll_inbound_plan (
|
||||
UNIQUE KEY uk_ll_inbound_plan_container (tenant_id,container_no)
|
||||
) ENGINE=InnoDB COMMENT='尾程入库计划表:每个租户的每个集装箱生成一张入库单';
|
||||
|
||||
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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_inbound_plan_line (
|
||||
id BIGINT NOT NULL COMMENT '入库计划明细ID(雪花ID)',
|
||||
plan_id BIGINT NOT NULL COMMENT '入库计划ID',
|
||||
@@ -440,6 +443,7 @@ CREATE TABLE IF NOT EXISTS ll_order (
|
||||
relabel_order_no VARCHAR(128) NULL COMMENT '换标单号',
|
||||
relabel_status VARCHAR(32) NULL COMMENT '换标状态',
|
||||
pallet_merge_flag VARCHAR(32) NULL COMMENT '合托标记',
|
||||
pallet_merge_batch_id VARCHAR(64) NULL COMMENT '平台预标记合托分组号',
|
||||
current_status VARCHAR(32) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '当前业务状态',
|
||||
private_remark VARCHAR(1000) NULL COMMENT '备注(仅自己)',
|
||||
customer_remark VARCHAR(1000) NULL COMMENT '备注(客户)',
|
||||
@@ -457,6 +461,9 @@ CREATE TABLE IF NOT EXISTS ll_order (
|
||||
KEY idx_ll_order_list (tenant_id,current_status,created_at)
|
||||
) ENGINE=InnoDB COMMENT='尾程订单主表';
|
||||
|
||||
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_order_detail (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单明细主键',
|
||||
order_id BIGINT NOT NULL COMMENT '尾程订单ID',
|
||||
|
||||
Reference in New Issue
Block a user