This commit is contained in:
2026-08-31 14:39:40 +08:00
parent a4429f95b7
commit a4189a0074
25 changed files with 345 additions and 64 deletions
@@ -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',
@@ -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")
@@ -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) {}
@@ -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;
@@ -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);
}
}
@@ -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;
}
@@ -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," +
@@ -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,
@@ -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) {
@@ -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)
@@ -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&lt;&gt;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),
@@ -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);
@@ -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();
@@ -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"));
}
}
@@ -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>"));
}
}
@@ -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
@@ -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
+7
View File
@@ -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',