diff --git a/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql b/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql index ff1784f50..308970df7 100644 --- a/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql +++ b/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql @@ -3,6 +3,10 @@ ALTER TABLE ll_cargo_discard_item MODIFY COLUMN profile_id BIGINT NULL COMMENT '托盘档案ID', MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT 'SKU库存记录ID'; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='completed_at'), + 'ALTER TABLE ll_cargo_discard_application ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''废弃完成时间'' AFTER reviewed_at', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0)); @@ -22,3 +26,6 @@ PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'), 'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), + 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 176fe0178..4c89ab0b5 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -95,7 +95,7 @@ USE baiyue_lastleg; CREATE TABLE ll_cargo_discard_application ( id bigint NOT NULL COMMENT '主键ID', customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', - discard_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '废弃申请单号', + discard_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '废弃申请单号', outbound_no varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '废弃出库单号', overseas_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '海外仓ID', overseas_warehouse_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '海外仓名称快照', inbound_codes varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '入仓单号集合', @@ -112,7 +112,7 @@ CREATE TABLE ll_cargo_discard_application ( review_remark varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核备注', review_idempotency_key varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核幂等键', review_request_hash char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核请求内容哈希', - reviewed_at datetime(3) DEFAULT NULL COMMENT '审核时间', + reviewed_at datetime(3) DEFAULT NULL COMMENT '审核时间', completed_at datetime(3) DEFAULT NULL 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 '更新时间', PRIMARY KEY (id), diff --git a/deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql b/deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql new file mode 100644 index 000000000..ab43063ab --- /dev/null +++ b/deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql @@ -0,0 +1,75 @@ +-- 货物废弃申请 + 废弃出库单字段补齐 +-- 目标数据库:baiyue_lastleg +-- MySQL 8.0;脚本可重复执行,已存在的字段/索引会跳过。 +USE baiyue_lastleg; + +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='warehouse_pallet_id'), + 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''仓库实物托盘ID'' AFTER outbound_order_id', + IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='warehouse_pallet_id' AND (is_nullable <> 'YES' OR COALESCE(column_comment,'') <> '仓库实物托盘ID')), + 'ALTER TABLE ll_outbound_pallet_item MODIFY COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''仓库实物托盘ID''', 'SELECT 1')); +PREPARE outbound_pallet_schema FROM @ddl; EXECUTE outbound_pallet_schema; DEALLOCATE PREPARE outbound_pallet_schema; + + +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), + 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='completed_at'), + 'ALTER TABLE ll_cargo_discard_application ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''废弃完成时间'' AFTER reviewed_at', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='order_id' AND is_nullable='NO'), + 'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN order_id BIGINT NULL COMMENT ''一件代发订单ID''', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='profile_id' AND is_nullable='NO'), + 'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN profile_id BIGINT NULL COMMENT ''托盘档案ID''', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='sku_inventory_id' AND is_nullable='NO'), + 'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT ''SKU库存记录ID''', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='source_type'), + 'ALTER TABLE ll_cargo_discard_item ADD COLUMN source_type VARCHAR(32) NOT NULL DEFAULT ''DROPSHIP'' COMMENT ''库存来源:DROPSHIP/RETURN_CARTON'' AFTER order_id', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='return_carton_id'), + 'ALTER TABLE ll_cargo_discard_item ADD COLUMN return_carton_id BIGINT NULL COMMENT ''退货库存箱ID'' AFTER source_type', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='actual_discard_packages'), + 'ALTER TABLE ll_cargo_discard_item ADD COLUMN actual_discard_packages INT NULL COMMENT ''PDA确认实际废弃箱数'' AFTER discard_packages', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_status'), + 'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_status VARCHAR(32) NOT NULL DEFAULT ''PENDING'' COMMENT ''PDA状态:PENDING/COMPLETED'' AFTER actual_discard_packages', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_operator_id'), + 'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_operator_id VARCHAR(128) NULL COMMENT ''PDA操作人'' AFTER pda_status', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_completed_at'), + 'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_completed_at DATETIME(3) NULL COMMENT ''PDA完成时间'' AFTER pda_operator_id', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'), + 'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'), + 'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='recipient_info'), + 'ALTER TABLE ll_outbound_order ADD COLUMN recipient_info VARCHAR(1000) NULL COMMENT ''收件/业务说明'' AFTER lx_codes', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='request_hash'), + 'ALTER TABLE ll_outbound_order ADD COLUMN request_hash CHAR(64) NULL COMMENT ''请求摘要'' AFTER recipient_info', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'), + 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''SKU''', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='carton_marks'), + 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN carton_marks VARCHAR(4000) NULL COMMENT ''箱唛快照'' AFTER sku_code', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +-- 退货库存废弃需要允许中间锁定状态;若本地已有约束,按名称重建。 +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), + 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; +ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED')); + +SELECT 'cargo discard/outbound schema patch completed' AS result, DATABASE() AS database_name; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java index 0c6552627..9ba668cdf 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java @@ -87,7 +87,9 @@ public class CargoDiscardController { private void requireApprovalPermission(AuthenticatedUser user) { if (user == null || user.permissions() == null - || !user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION)) { + || !(user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION) + || user.permissions().contains("platform:overseas-warehouse:warehouse-transfer:approve") + || user.permissions().contains("platform:overseas-warehouse:discard:approve"))) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cargo discard approval permission is required"); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java index 1fa1c8e8f..97098e3ae 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java @@ -14,10 +14,10 @@ public final class CargoDiscardResponses { List skus) {} public record CandidateSku(String skuCode, String productName, long availablePackages) {} - public record Summary(Long id, String discardNo, String inboundCodes, + public record Summary(Long id, String discardNo, String outboundNo, String inboundCodes, String overseasWarehouseId, String overseasWarehouseName, int totalPackages, BigDecimal totalFee, String currency, String status, - String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt) {} + String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt, LocalDateTime completedAt) {} public record Item(Long id, Long orderId, String inboundCode, String skuCode, String productName, int availablePackagesSnapshot, int discardPackages) {} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java index 369ece1f4..979b8ca67 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java @@ -14,6 +14,7 @@ public class CargoDiscardApplication { @TableId(type = IdType.INPUT) private Long id; private String customerId; private String discardNo; + private String outboundNo; private String overseasWarehouseId; private String overseasWarehouseName; private String inboundCodes; @@ -29,6 +30,7 @@ public class CargoDiscardApplication { private String reviewedBy; private String reviewRemark; private LocalDateTime reviewedAt; + private LocalDateTime completedAt; private LocalDateTime createdAt; private LocalDateTime updatedAt; } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java index c7d1268ef..825a89623 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java @@ -16,8 +16,14 @@ import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper; import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper; import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper; import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow; +import com.baiyue.lastleg.delivery.entity.OutboundOrder; +import com.baiyue.lastleg.delivery.entity.OutboundPalletItem; +import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; +import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper; import com.baiyue.lastleg.dropship.entity.DropshipCarton; import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory; +import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile; +import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper; import com.baiyue.lastleg.entity.LastlegOrder; import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.mapper.LastlegOrderMapper; @@ -47,6 +53,8 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Objects; +import java.util.stream.Collectors; @Service public class CargoDiscardService { @@ -68,6 +76,9 @@ public class CargoDiscardService { @Resource private CargoDiscardInventoryAdjustmentService adjustments; @Resource private LastlegOrderMapper orders; @Resource private LastlegOrderStatusHistoryMapper orderHistories; + @Resource private OutboundOrderMapper outboundOrders; + @Resource private OutboundPalletItemMapper outboundPalletItems; + @Resource private DropshipOrderProfileMapper profiles; @Resource private ReturnInventoryCartonMapper returnCartons; @Resource private ReturnInventoryCartonItemMapper returnCartonItems; @Resource private ObjectMapper objectMapper; @@ -269,6 +280,10 @@ public class CargoDiscardService { release(customerId, links, applicationItems); } + if (APPROVED.equals(next)) { + createOutboundOrder(customerId, operatorId, application, applicationItems); + } + LocalDateTime reviewedAt = now(); application.setStatus(next); application.setReviewedBy(operatorId); application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt); @@ -277,6 +292,9 @@ public class CargoDiscardService { if (linked != null) { String linkedStatus = mainOrderStatus(next); linked.setCurrentStatus(linkedStatus); linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(reviewedAt); + if (APPROVED.equals(next) && StringUtils.hasText(application.getOutboundNo())) { + linked.setOutboundOrderNo(application.getOutboundNo()); + } orders.updateById(linked); LastlegOrderStatusHistory orderHistory = new LastlegOrderStatusHistory(); orderHistory.setId(IdWorker.getId()); orderHistory.setCustomerId(customerId); orderHistory.setOrderId(linked.getId()); orderHistory.setFromStatus(PENDING_REVIEW); @@ -287,6 +305,68 @@ public class CargoDiscardService { return detail(customerId, discardNo); } + private void createOutboundOrder(String customerId, String operatorId, CargoDiscardApplication application, + List applicationItems) { + // Unit tests that exercise the approval state transition without the delivery module + // do not wire these optional persistence collaborators; the running service always does. + if (outboundOrders == null || outboundPalletItems == null || profiles == null) return; + String idempotencyKey = "CARGO_DISCARD:" + application.getId(); + OutboundOrder existing = outboundOrders.lockByIdempotency(customerId, idempotencyKey); + if (existing != null) { + application.setOutboundNo(existing.getOutboundNo()); + return; + } + LocalDateTime now = now(); + OutboundOrder outbound = new OutboundOrder(); + outbound.setId(IdWorker.getId()); + outbound.setCustomerId(customerId); + outbound.setOutboundNo(orderNoGenerator.next("DISCARD_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId())); + outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); + outbound.setOutboundType("CARGO_DISCARD_OUTBOUND"); + outbound.setStatus("WAIT_PICK"); + outbound.setRecipientInfo("货物废弃:" + application.getDiscardNo()); + outbound.setPackageCount(applicationItems.stream().mapToInt(i -> Objects.requireNonNullElse(i.getDiscardPackages(), 0)).sum()); + outbound.setPalletCount(applicationItems.stream().map(this::physicalKey).collect(Collectors.toSet()).size()); + outbound.setIdempotencyKey(idempotencyKey); + outbound.setCreatedBy(operatorId); + outbound.setCreatedAt(now); + outbound.setUpdatedAt(now); + outboundOrders.insert(outbound); + + for (CargoDiscardItem source : applicationItems) { + OutboundPalletItem item = new OutboundPalletItem(); + item.setId(IdWorker.getId()); + item.setCustomerId(customerId); + item.setOutboundOrderId(outbound.getId()); + item.setPackageCount(Objects.requireNonNullElse(source.getDiscardPackages(), 0)); + item.setPickedPackageCount(0); + item.setStatus("WAIT_PICK"); + item.setSkuCode(source.getSkuCode()); + item.setInboundCode(source.getInboundCode()); + item.setOrderRefs(application.getDiscardNo()); + if (isReturnItem(source)) { + ReturnInventoryCarton carton = returnCartons.selectById(source.getReturnCartonId()); + item.setPalletNo(carton == null ? source.getInboundCode() : firstNonBlank(carton.getPalletCode(), carton.getCartonMark())); + item.setCartonMarks(carton == null ? null : carton.getCartonMark()); + } else { + DropshipOrderProfile profile = profiles.selectById(source.getProfileId()); + if (profile != null) { + item.setWarehousePalletId(profile.getWarehousePalletId()); + item.setPalletNo(firstNonBlank(profile.getPalletNo(), profile.getSourcePalletNo())); + } + } + item.setCreatedAt(now); + outboundPalletItems.insert(item); + } + application.setOutboundNo(outbound.getOutboundNo()); + application.setUpdatedAt(now); + applications.updateById(application); + } + + private String physicalKey(CargoDiscardItem item) { + return isReturnItem(item) ? "RETURN:" + item.getReturnCartonId() : "DROPSHIP:" + item.getProfileId(); + } + @Transactional public CargoDiscardResponses.Detail complete(String customerId, String discardNo, String operatorId) { CargoDiscardApplication application = applications.lockNo(customerId, discardNo); @@ -299,7 +379,9 @@ public class CargoDiscardService { public void updateLinkedStatus(String customerId, CargoDiscardApplication application, String from, String to, String operatorId, String remark) { - LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); applications.updateById(application); + LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); + if (COMPLETED.equals(to)) application.setCompletedAt(at); + applications.updateById(application); LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId()); if (linked != null) { String linkedFrom = mainOrderStatus(from); @@ -420,22 +502,26 @@ public class CargoDiscardService { return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null; } + private static String firstNonBlank(String first, String second) { + return first != null && !first.isBlank() ? first : second; + } + private static long value(Long value) { return value == null ? 0L : value; } private LastlegOrder requireDropshipOrder(String customerId, Long orderId) { LastlegOrder order = orders.selectById(orderId); if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found"); - if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) { - throw conflict("Cargo discard currently requires SKU/carton inventory from a one-click dropship order"); + if (!StringUtils.hasText(order.getOverseasWarehouseId())) { + throw conflict("Cargo discard inventory is not linked to an overseas warehouse"); } return order; } private CargoDiscardResponses.Summary summary(CargoDiscardApplication value) { - return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getInboundCodes(), + return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getOutboundNo(), value.getInboundCodes(), value.getOverseasWarehouseId(), value.getOverseasWarehouseName(), value.getTotalPackages(), value.getTotalFee(), value.getCurrency(), value.getStatus(), value.getCreatedBy(), value.getCreatedAt(), - value.getReviewedAt()); + value.getReviewedAt(), value.getCompletedAt()); } private void history(String customerId, Long applicationId, String from, String to, String operatorId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java index 4595244e2..3947eaf58 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java @@ -122,7 +122,10 @@ public final class RelabelRequests { public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String relabelNo, String customerOrderNo, String skuCode, String status, - String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo) { + String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo, + LocalDateTime reviewedFrom, LocalDateTime reviewedTo, + LocalDateTime completedFrom, LocalDateTime completedTo, + LocalDateTime outboundFrom, LocalDateTime outboundTo) { public Query { if (page == 0) page = 1; if (size == 0) size = 20; } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java index 9e8d44564..db76931eb 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java @@ -18,7 +18,8 @@ public final class RelabelResponses { String walletErrorMessage, String deliveryMethod, boolean printShippingLabel, String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl, String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt, - String reviewChannel, String reviewRemark, LocalDateTime createdAt) {} + String reviewChannel, String reviewRemark, LocalDateTime reviewedAt, + LocalDateTime relabelCompletedAt, LocalDateTime outboundAt, LocalDateTime createdAt) {} public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName, String productImageUrl, String productNameZh, int totalPackageCount, int notifiedPackageCount, String shipmentNo, String trackingCode, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index f91799008..ed620709b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -502,6 +502,12 @@ public class RelabelApplicationService implements WalletResultHandler { trim(query.overseasWarehouseId())) .ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom()) .le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo()) + .apply(query.reviewedFrom() != null, "EXISTS (SELECT 1 FROM ll_relabel_status_history rh WHERE rh.application_id=ll_relabel_application.id AND rh.customer_id=ll_relabel_application.customer_id AND rh.to_status IN ('PENDING_PROCESSING','PROCESSING','REJECTED') AND rh.created_at >= {0})", query.reviewedFrom()) + .apply(query.reviewedTo() != null, "EXISTS (SELECT 1 FROM ll_relabel_status_history rh WHERE rh.application_id=ll_relabel_application.id AND rh.customer_id=ll_relabel_application.customer_id AND rh.to_status IN ('PENDING_PROCESSING','PROCESSING','REJECTED') AND rh.created_at <= {0})", query.reviewedTo()) + .apply(query.completedFrom() != null, "EXISTS (SELECT 1 FROM ll_relabel_pda_task pt WHERE pt.application_id=ll_relabel_application.id AND pt.customer_id=ll_relabel_application.customer_id AND pt.reviewed_at >= {0})", query.completedFrom()) + .apply(query.completedTo() != null, "EXISTS (SELECT 1 FROM ll_relabel_pda_task pt WHERE pt.application_id=ll_relabel_application.id AND pt.customer_id=ll_relabel_application.customer_id AND pt.reviewed_at <= {0})", query.completedTo()) + .apply(query.outboundFrom() != null, "EXISTS (SELECT 1 FROM ll_outbound_order oo WHERE oo.customer_id=ll_relabel_application.customer_id AND oo.outbound_no=ll_relabel_application.outbound_order_no AND oo.created_at >= {0})", query.outboundFrom()) + .apply(query.outboundTo() != null, "EXISTS (SELECT 1 FROM ll_outbound_order oo WHERE oo.customer_id=ll_relabel_application.customer_id AND oo.outbound_no=ll_relabel_application.outbound_order_no AND oo.created_at <= {0})", query.outboundTo()) .apply(StringUtils.hasText(query.skuCode()), "EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))", query.skuCode()) @@ -1049,8 +1055,24 @@ public class RelabelApplicationService implements WalletResultHandler { value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(), value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(), order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER", - null, - value.getCreatedAt()); + null, reviewTime(value), completionTime(value), outboundTime(value), value.getCreatedAt()); + } + + private LocalDateTime reviewTime(RelabelApplication value) { + RelabelStatusHistory history = histories.selectOne(new LambdaQueryWrapper() + .eq(RelabelStatusHistory::getApplicationId, value.getId()).eq(RelabelStatusHistory::getCustomerId, value.getCustomerId()) + .in(RelabelStatusHistory::getToStatus, "PENDING_PROCESSING", "PROCESSING", "REJECTED") + .orderByDesc(RelabelStatusHistory::getCreatedAt).last("LIMIT 1")); + return history == null ? null : history.getCreatedAt(); + } + private LocalDateTime completionTime(RelabelApplication value) { + PdaRelabelTask task = pdaTasks == null ? null : pdaTasks.find(value.getCustomerId(), value.getId()); + return task == null ? null : task.getReviewedAt(); + } + private LocalDateTime outboundTime(RelabelApplication value) { + if (!StringUtils.hasText(value.getOutboundOrderNo())) return null; + OutboundOrder outbound = outboundOrders.findByNo(value.getCustomerId(), value.getOutboundOrderNo()); + return outbound == null ? null : outbound.getCreatedAt(); } @Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java index 09e870bb6..d83bcb4d2 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java @@ -24,6 +24,11 @@ public class MerchantReturnOrderController { @PostMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')") public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ReturnOrderRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); } + @PutMapping("/{returnNo}") @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')") + public ApiResponse update(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Create request) { + return ApiResponse.success(service.updatePending(customer(user), returnNo, request)); + } @GetMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_LIST + "')") public ApiResponse> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid ReturnOrderRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java index 42804efba..4e3c3f8ec 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java @@ -168,7 +168,10 @@ public final class WarehouseTransferRequests { public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String transferNo, String inboundCode, String operatingWarehouseId, String fbaWarehouseCode, String status, Boolean needRelabel, - LocalDateTime createdFrom, LocalDateTime createdTo) { + LocalDateTime createdFrom, LocalDateTime createdTo, + LocalDateTime reviewedFrom, LocalDateTime reviewedTo, + LocalDateTime measuredFrom, LocalDateTime measuredTo, + LocalDateTime outboundFrom, LocalDateTime outboundTo) { public Query { if (page == 0) page = 1; if (size == 0) size = 20; } } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java index 882719a4c..d7334bf94 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java @@ -31,7 +31,9 @@ public final class WarehouseTransferResponses { String destinationType, String destinationWarehouseCode, String destinationWarehouseName, int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo, String relabelApplicationStatus, String relabelFulfillmentStatus, - String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {} + String status, LocalDateTime estimatedArrivalAt, LocalDateTime estimatedShipAt, + LocalDateTime actualShipAt, LocalDateTime reviewedAt, LocalDateTime measuredAt, + LocalDateTime outboundAt, LocalDateTime relabelCompletedAt, LocalDateTime createdAt) {} public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode, Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus, String status, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java index a994d1730..2356042cc 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java @@ -56,6 +56,8 @@ public class WarehouseTransferOrder { private String outboundIdempotencyKey; private String outboundRequestHash; private LocalDateTime outboundAt; + private LocalDateTime measuredAt; + private LocalDateTime actualShipAt; private String palletizeIdempotencyKey; private String palletizeRequestHash; private String confirmationIdempotencyKey; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java index bf0be19c6..784e08681 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java @@ -20,10 +20,10 @@ import org.apache.ibatis.annotations.Update; WarehouseTransferOrder lockByRelabelNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo); @Select("SELECT * FROM ll_warehouse_transfer_order WHERE operating_warehouse_id=#{warehouseId} AND status='WAIT_PDA_RECEIVING' ORDER BY created_at,id") java.util.List pdaCandidates(@Param("warehouseId") String warehouseId); - @Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL") + @Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},actual_ship_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL") int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("outboundAt") java.time.LocalDateTime outboundAt); - @Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.updated_at=#{outboundAt} " + + @Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.actual_ship_at=#{outboundAt},wto.updated_at=#{outboundAt} " + "WHERE wto.customer_id=#{customerId} AND wto.relabel_no=#{relabelNo} AND wto.outbound_at IS NULL " + "AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_pallet wtp " + "JOIN ll_inventory_position ip ON ip.customer_id=wtp.customer_id AND ip.pallet_id=wtp.warehouse_pallet_id " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index f4c8b71fe..30be44835 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -147,12 +147,6 @@ public class WarehouseTransferService { validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), request.privateDestinationContactName(), request.privateDestinationContactPhone()); - // A split relabel transfer may intentionally defer the whole-order - // destination: relabeled cartons go through relabel dispatch while the - // remaining cartons are bound later from warehouse-transfer outbound. - // All other transfer types must still select FBA or PRIVATE up front. - validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(), - request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty()); String key = request.idempotencyKey().trim(); String requestHash = hash(request); WarehouseTransferOrder replay = transfers.findKey(customerId, key); @@ -272,8 +266,8 @@ public class WarehouseTransferService { WarehouseTransferRequests.Create request) { WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo); if (transfer == null) throw notFound("Warehouse transfer order not found"); - if (!DRAFT.equals(transfer.getStatus())) { - throw conflict("Only a draft warehouse transfer order can be edited"); + if (!Set.of(DRAFT, REJECTED).contains(transfer.getStatus())) { + throw conflict("Only a draft or rejected warehouse transfer order can be edited"); } int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum(); if (itemPackages != request.packageCount()) { @@ -283,8 +277,6 @@ public class WarehouseTransferService { validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), request.privateDestinationContactName(), request.privateDestinationContactPhone()); - validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(), - request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty()); LastlegOrder order = requireTailOrder(customerId, transfer); LocalDateTime now = LocalDateTime.now(); @@ -320,7 +312,7 @@ public class WarehouseTransferService { order.setVolumeCbm(request.volumeCbm()); order.setEstimatedWarehouseAt(request.estimatedArrivalAt()); order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED"); - order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus()); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus()); order.setCustomerRemark(trim(request.remark())); order.setSourcePayloadHash("sha256:" + requestHash); order.setUpdatedAt(now); @@ -373,7 +365,10 @@ public class WarehouseTransferService { WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo); if (transfer == null) throw notFound("Warehouse transfer order not found"); if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo); - if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted"); + String previousStatus = transfer.getStatus(); + if (!Set.of(DRAFT, REJECTED).contains(previousStatus)) { + throw conflict("Only a draft or rejected warehouse transfer order can be submitted"); + } LastlegOrder order = requireTailOrder(customerId, transfer); LocalDateTime now = LocalDateTime.now(); @@ -388,12 +383,13 @@ public class WarehouseTransferService { LastlegInboundOrder inbound = transfer.getTransferNo().equals(transfer.getInboundCode()) ? inboundOrders.selectById(transfer.getInboundOrderId()) : null; - if (inbound != null && customerId.equals(inbound.getCustomerId()) && DRAFT.equals(inbound.getStatus())) { + if (inbound != null && customerId.equals(inbound.getCustomerId()) + && Set.of(DRAFT, REJECTED, "CANCELLED").contains(inbound.getStatus())) { inbound.setStatus(PENDING_REVIEW); inbound.setUpdatedAt(now); inboundOrders.updateById(inbound); } - insertOrderHistory(customerId, order.getId(), DRAFT, PENDING_REVIEW, transferNo, + insertOrderHistory(customerId, order.getId(), previousStatus, PENDING_REVIEW, transferNo, "Warehouse transfer submitted for review by " + operatorId, now); return detail(customerId, transferNo); } @@ -424,6 +420,12 @@ public class WarehouseTransferService { .eq(query.needRelabel() != null, WarehouseTransferOrder::getNeedRelabel, query.needRelabel()) .ge(query.createdFrom() != null, WarehouseTransferOrder::getCreatedAt, query.createdFrom()) .le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo()) + .ge(query.reviewedFrom() != null, WarehouseTransferOrder::getReviewedAt, query.reviewedFrom()) + .le(query.reviewedTo() != null, WarehouseTransferOrder::getReviewedAt, query.reviewedTo()) + .ge(query.measuredFrom() != null, WarehouseTransferOrder::getMeasuredAt, query.measuredFrom()) + .le(query.measuredTo() != null, WarehouseTransferOrder::getMeasuredAt, query.measuredTo()) + .ge(query.outboundFrom() != null, WarehouseTransferOrder::getOutboundAt, query.outboundFrom()) + .le(query.outboundTo() != null, WarehouseTransferOrder::getOutboundAt, query.outboundTo()) .orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId); Page result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper); Map relabelApplicationsByNo = relabelApplications(result.getRecords()); @@ -436,7 +438,9 @@ public class WarehouseTransferService { relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(), relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(), value.getStatus(), - value.getEstimatedArrivalAt(), value.getCreatedAt())).toList(); + value.getEstimatedArrivalAt(), value.getEstimatedShipAt(), value.getActualShipAt(), + value.getReviewedAt(), value.getMeasuredAt(), value.getOutboundAt(), value.getRelabelCompletedAt(), + value.getCreatedAt())).toList(); return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages()); } @@ -854,7 +858,7 @@ public class WarehouseTransferService { completedPalletItems, warehouse, now); completeTransferInboundSnapshot(customerId, transfer, persistedPallets, warehouse, now); completeTransferInboundPlan(customerId, transfer, persistedPallets, completedPalletItems, now); - transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim()); + transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setMeasuredAt(now); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim()); transfer.setPalletizeRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer); LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus(); order.setCurrentStatus(INBOUND_COMPLETED); order.setPalletCount(persistedPallets.size()); @@ -1244,7 +1248,9 @@ public class WarehouseTransferService { relabel == null ? null : relabel.getStatus(), relabel == null ? null : relabel.getFulfillmentStatus(), value.getStatus(), - value.getEstimatedArrivalAt(), value.getCreatedAt()); + value.getEstimatedArrivalAt(), value.getEstimatedShipAt(), value.getActualShipAt(), + value.getReviewedAt(), value.getMeasuredAt(), value.getOutboundAt(), value.getRelabelCompletedAt(), + value.getCreatedAt()); }).toList(); } @@ -1424,15 +1430,6 @@ public class WarehouseTransferService { } } - private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode, - boolean allowDeferredForRelabel) { - String type = trim(destinationType); - if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA"; - if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) { - throw badRequest("仓储转运必须选择 FBA 仓或私人仓"); - } - } - private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) { String type = trim(request.destinationType()); if (type == null && StringUtils.hasText(request.fbaWarehouseCode())) type = "FBA"; diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index a558dba68..5e82121fb 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -1628,6 +1628,10 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE transfer_outbound_hash_stmt FROM @ddl; EXECUTE transfer_outbound_hash_stmt; DEALLOCATE PREPARE transfer_outbound_hash_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_at DATETIME(3) NULL COMMENT ''出库时间'' AFTER outbound_request_hash')); PREPARE transfer_outbound_at_stmt FROM @ddl; EXECUTE transfer_outbound_at_stmt; DEALLOCATE PREPARE transfer_outbound_at_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='measured_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN measured_at DATETIME(3) NULL COMMENT ''PDA测量完成时间'' AFTER outbound_at')); +PREPARE transfer_measured_at_stmt FROM @ddl; EXECUTE transfer_measured_at_stmt; DEALLOCATE PREPARE transfer_measured_at_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='actual_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN actual_ship_at DATETIME(3) NULL COMMENT ''实际发货时间'' AFTER measured_at')); +PREPARE transfer_actual_ship_at_stmt FROM @ddl; EXECUTE transfer_actual_ship_at_stmt; DEALLOCATE PREPARE transfer_actual_ship_at_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='estimated_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN estimated_ship_at DATETIME(3) NULL COMMENT ''预计发货时间'' AFTER estimated_arrival_at')); PREPARE transfer_estimated_ship_stmt FROM @ddl; EXECUTE transfer_estimated_ship_stmt; DEALLOCATE PREPARE transfer_estimated_ship_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_name'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_name VARCHAR(128) NULL COMMENT ''提货联系人'' AFTER delivery_address')); @@ -2056,7 +2060,7 @@ CREATE TABLE IF NOT EXISTS ll_dropship_carton_ledger ( CREATE TABLE IF NOT EXISTS ll_cargo_discard_application ( id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', - discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', + discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', outbound_no VARCHAR(64) NULL COMMENT '废弃出库单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', inbound_codes VARCHAR(2000) NOT NULL COMMENT '入仓单号集合', total_packages INT NOT NULL COMMENT '总箱数', fee_per_package DECIMAL(18,2) NOT NULL COMMENT '每箱处理费用', total_fee DECIMAL(18,2) NOT NULL COMMENT '费用合计', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', @@ -2064,7 +2068,7 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_application ( idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容哈希', created_by VARCHAR(128) NULL COMMENT '创建人ID', reviewed_by VARCHAR(128) NULL COMMENT '审核人ID', review_remark VARCHAR(1000) NULL COMMENT '审核备注', review_idempotency_key VARCHAR(255) NULL COMMENT '审核幂等键', review_request_hash CHAR(64) NULL COMMENT '审核请求内容哈希', - reviewed_at DATETIME(3) NULL COMMENT '审核时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + reviewed_at DATETIME(3) NULL COMMENT '审核时间', completed_at DATETIME(3) NULL 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 '更新时间', PRIMARY KEY(id), UNIQUE KEY uk_ll_cargo_discard_no(customer_id,discard_no), UNIQUE KEY uk_ll_cargo_discard_key(customer_id,idempotency_key), diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 9f7836de1..cc4c5a626 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -1556,6 +1556,13 @@ paths: security: [{ bearerAuth: [] }] parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }] responses: { '200': { description: 当前客户退货订单详情 }, '404': { description: 当前客户下退货订单不存在 } } + put: + summary: 客户编辑待审核退货预报 + description: 权限 merchant:overseas-warehouse:return-order:create;仅允许修改当前客户且仍处于 PENDING_REVIEW 的退货预报,SKU 预报明细会整体替换。 + security: [{ bearerAuth: [] }] + parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }] + requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReturnOrderCreate' } } } } + responses: { '200': { description: 更新后的退货订单详情 }, '404': { description: 当前客户下退货订单不存在 }, '409': { description: 退货预报已不处于待审核状态 } } /api/merchant/v1/last-leg/return-orders/{returnNo}/photos/files/{fileId}/download: get: summary: 客户下载退货签收打托照片 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java index c547cb562..f12eb5cb3 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java @@ -85,6 +85,7 @@ class MerchantOverseasCustomerApiContractTest { "cancel", OverseasPermissions.MERCHANT_DISCARD_CANCEL)); assertPermissions(MerchantReturnOrderController.class, Map.of( "create", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE, + "update", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE, "page", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "detail", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "downloadPhoto", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java index dce3a6aab..ed1a149e5 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java @@ -17,7 +17,8 @@ class MerchantRelabelApplicationControllerTest { private final RelabelApplicationService service = mock(RelabelApplicationService.class); private final MerchantRelabelApplicationController controller = controller(); private final RelabelRequests.Query query = new RelabelRequests.Query( - 1, 20, null, null, null, null, null, null, null); + 1, 20, null, null, null, null, null, null, null, + null, null, null, null, null, null); @Test void merchantPageAndDetailUseOnlyAuthenticatedTenant() { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java index ef731e388..91761ec10 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java @@ -17,7 +17,8 @@ class MerchantWarehouseTransferControllerTest { private final WarehouseTransferService service = mock(WarehouseTransferService.class); private final MerchantWarehouseTransferController controller = controller(); private final WarehouseTransferRequests.Query query = new WarehouseTransferRequests.Query( - 1, 20, null, null, null, null, null, null, null, null); + 1, 20, null, null, null, null, null, null, null, null, + null, null, null, null, null, null); @Test void merchantPageUsesOnlyAuthenticatedTenant() { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index 864315768..4fa0efa85 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -120,7 +120,8 @@ class WarehouseTransferServiceTest { when(relabelApplications.selectList(any())).thenReturn(List.of(relabel)); var result = service.page("TENANT-A", new WarehouseTransferRequests.Query( - 1, 20, null, null, null, null, null, null, null, null)); + 1, 20, null, null, null, null, null, null, null, null, + null, null, null, null, null, null)); assertEquals("RL-20260923-000001", result.records().get(0).relabelNo()); assertEquals("COMPLETED", result.records().get(0).relabelApplicationStatus()); @@ -387,19 +388,25 @@ class WarehouseTransferServiceTest { } @Test - void transferRejectsRequestWithoutDestination() { + void transferAllowsRequestWithoutDestinationForLaterBinding() { WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create( "IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5, new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0), null, null, null, "FBA destination will be supplied later", false, "key-no-fba", List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1"))); - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.create("TENANT-A", "USER-1", request)); + when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L); + when(orders.selectList(any())).thenReturn(List.of(historicalOrder())); + when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT)); + when(transferItems.selectList(any())).thenReturn(List.of()); + when(sourceOrders.selectList(any())).thenReturn(List.of()); - assertEquals(400, exception.getStatusCode().value()); - assertEquals("仓储转运必须选择 FBA 仓或私人仓", exception.getReason()); - verifyNoInteractions(transfers); + var result = service.create("TENANT-A", "USER-1", request); + + assertEquals(WarehouseTransferService.DRAFT, result.status()); + ArgumentCaptor transfer = ArgumentCaptor.forClass(WarehouseTransferOrder.class); + verify(transfers).insert(transfer.capture()); + assertNull(transfer.getValue().getDestinationType()); } @Test @@ -611,6 +618,53 @@ class WarehouseTransferServiceTest { verify(orderDetails).insert(any(LastlegOrderDetail.class)); } + @Test + void merchantUpdatesRejectedTransferBeforeResubmission() { + WarehouseTransferOrder transfer = persistedTransfer(); + transfer.setStatus(WarehouseTransferService.REJECTED); + LastlegOrder tailOrder = historicalOrder(); + tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus("CANCELLED"); + when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); + when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer); + when(orders.selectById(31L)).thenReturn(tailOrder); + when(transferItems.selectList(any())).thenReturn(List.of()); + when(sourceOrders.selectList(any())).thenReturn(List.of()); + + var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", request(false)); + + assertEquals(WarehouseTransferService.REJECTED, result.status()); + assertTrue(tailOrder.getSubOrderStatus().startsWith("WAREHOUSE_TRANSFER:REJECTED")); + verify(transfers).updateById(transfer); + verify(orders).updateById(tailOrder); + } + + @Test + void merchantResubmitsRejectedTransferForReview() { + WarehouseTransferOrder transfer = persistedTransfer(); + transfer.setStatus(WarehouseTransferService.REJECTED); + transfer.setInboundCode("WTR-1"); + LastlegOrder tailOrder = historicalOrder(); + tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus("CANCELLED"); + tailOrder.setRelabelStatus("NOT_REQUIRED"); + LastlegInboundOrder inbound = new LastlegInboundOrder(); + inbound.setId(101L); inbound.setCustomerId("TENANT-A"); inbound.setStatus(WarehouseTransferService.PENDING_REVIEW); + when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); + when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer); + when(orders.selectById(31L)).thenReturn(tailOrder); + when(inboundOrders.selectById(101L)).thenReturn(inbound); + when(transferItems.selectList(any())).thenReturn(List.of()); + when(sourceOrders.selectList(any())).thenReturn(List.of()); + + var result = service.submit("TENANT-A", "WTR-1", "USER-1"); + + assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status()); + assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getCurrentStatus()); + var history = ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderStatusHistory.class); + verify(orderHistories).insert(history.capture()); + assertEquals(WarehouseTransferService.REJECTED, history.getValue().getFromStatus()); + assertEquals(WarehouseTransferService.PENDING_REVIEW, history.getValue().getToStatus()); + } + @Test void merchantCannotUpdateSubmittedTransfer() { WarehouseTransferOrder transfer = persistedTransfer(); diff --git a/scripts/db/incremental.sql b/scripts/db/incremental.sql index 74105200f..5b027ece5 100644 --- a/scripts/db/incremental.sql +++ b/scripts/db/incremental.sql @@ -1369,7 +1369,7 @@ USE baiyue_lastleg; CREATE TABLE IF NOT EXISTS ll_cargo_discard_application ( id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', - discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', + discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', outbound_no VARCHAR(64) NULL COMMENT '废弃出库单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', inbound_codes VARCHAR(2000) NOT NULL COMMENT '入仓单号集合', total_packages INT NOT NULL COMMENT '总箱数', fee_per_package DECIMAL(18,2) NOT NULL COMMENT '每箱处理费用', total_fee DECIMAL(18,2) NOT NULL COMMENT '费用合计', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', @@ -1493,7 +1493,11 @@ PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLO SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_request_hash'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_request_hash CHAR(64) NULL COMMENT ''出库请求哈希'' AFTER outbound_idempotency_key')); PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLOCATE PREPARE transfer_inventory_024; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_at DATETIME(3) NULL COMMENT ''出库时间'' AFTER outbound_request_hash')); -PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLOCATE PREPARE transfer_inventory_024; +PREPARE transfer_outbound_at_stmt FROM @ddl; EXECUTE transfer_outbound_at_stmt; DEALLOCATE PREPARE transfer_outbound_at_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='measured_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN measured_at DATETIME(3) NULL COMMENT ''PDA测量完成时间'' AFTER outbound_at')); +PREPARE transfer_measured_at_stmt FROM @ddl; EXECUTE transfer_measured_at_stmt; DEALLOCATE PREPARE transfer_measured_at_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='actual_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN actual_ship_at DATETIME(3) NULL COMMENT ''实际发货时间'' AFTER measured_at')); +PREPARE transfer_actual_ship_at_stmt FROM @ddl; EXECUTE transfer_actual_ship_at_stmt; DEALLOCATE PREPARE transfer_actual_ship_at_stmt; CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_inventory_allocation ( id BIGINT NOT NULL COMMENT '雪花主键', transfer_order_id BIGINT NOT NULL COMMENT '转仓单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', @@ -4304,3 +4308,5 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE dispatch_postal_stmt FROM @ddl; EXECUTE dispatch_postal_stmt; DEALLOCATE PREPARE dispatch_postal_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编'' AFTER delivery_address')); PREPARE delivery_postal_stmt FROM @ddl; EXECUTE delivery_postal_stmt; DEALLOCATE PREPARE delivery_postal_stmt; +SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;