From 9ea03eb5401b15069046b9310097da82a32fd4a8 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 16 Oct 2026 17:43:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/海外区域统一增量-MySQL8.sql | 2 +- .../repository/RelabelStatusRepository.java | 43 ++++++++++++++ .../service/RelabelApplicationService.java | 47 ++++++++------- .../dto/WarehouseTransferRequests.java | 6 +- .../service/WarehouseTransferService.java | 23 ++++++-- .../RelabelApplicationServiceTest.java | 57 +++++++++++++++++++ .../service/WarehouseTransferServiceTest.java | 19 ++++++- 7 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelStatusRepository.java diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index e7f14803a..970c1e4a6 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -3270,7 +3270,7 @@ CREATE TABLE ll_warehouse_transfer_carton ( sku_items_json JSON NULL COMMENT '实收箱内SKU及逐项件数明细', id bigint NOT NULL COMMENT '预报箱主键', transfer_order_id bigint NOT NULL COMMENT '转运单ID', - transfer_item_id bigint NOT NULL COMMENT '转运申报明细ID', + transfer_item_id bigint DEFAULT NULL COMMENT '转运申报明细ID', customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', carton_mark varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '系统生成的一箱一码箱唛', customer_carton_mark varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户原箱唛,用于换标现场绑定', diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelStatusRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelStatusRepository.java new file mode 100644 index 000000000..61587d82f --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelStatusRepository.java @@ -0,0 +1,43 @@ +package com.baiyue.lastleg.relabel.repository; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baiyue.lastleg.entity.LastlegOrder; +import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; +import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; +import java.util.List; + +/** 封装换标状态命令的客户隔离查询与持久化。 */ +@Repository +public class RelabelStatusRepository { + @Resource private RelabelApplicationMapper applications; + @Resource private LastlegOrderMapper orders; + @Resource private ReturnOrderMapper returnOrders; + @Resource private ReturnInventoryCartonMapper returnCartons; + + /** 锁定申请,平台入口从单据解析客户。 */ + public RelabelApplication lockApplication(String customer, String no) { + return StringUtils.hasText(customer) ? applications.lockNo(customer, no) : applications.lockAnyByRelabelNo(no); + } + /** 读取申请主订单。 */ + public LastlegOrder findPrimaryOrder(Long id) { return orders.selectById(id); } + /** 保存申请状态及资金状态。 */ + public void saveApplication(RelabelApplication application) { applications.updateById(application); } + /** 保存关联订单状态。 */ + public void saveOrder(LastlegOrder order) { orders.updateById(order); } + /** 读取同一客户下关联的退货单。 */ + public List linkedReturnOrders(String customer, Long id) { + return returnOrders.selectList(new LambdaQueryWrapper() + .eq(ReturnOrder::getRelabelApplicationId, id).eq(ReturnOrder::getCustomerId, customer)); + } + /** 保存退货单换标状态。 */ + public void saveReturnOrder(ReturnOrder order) { returnOrders.updateById(order); } + /** 核对退货箱实际已换标数量。 */ + public long relabelledQuantity(Long id) { return returnCartons.totalRelabelledQuantity(id); } +} 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 17c6f9fcf..e0cab9da6 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 @@ -82,6 +82,7 @@ public class RelabelApplicationService implements WalletResultHandler { @Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers; @Resource private RelabelApplicationMapper applications; + @Resource private com.baiyue.lastleg.relabel.repository.RelabelStatusRepository statusRepository; @Resource private RelabelItemMapper items; @Resource private RelabelStatusHistoryMapper histories; @Resource private LastlegOrderMapper orders; @@ -621,6 +622,7 @@ public class RelabelApplicationService implements WalletResultHandler { Objects.requireNonNullElse(value.getPackageCount(), 0), value.getCurrentStatus())).toList(), evidence); } + /** 统一执行换标单审核、作业状态流转和作废。 */ @Transactional public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId, RelabelRequests.ChangeStatus request) { @@ -639,12 +641,11 @@ public class RelabelApplicationService implements WalletResultHandler { return changeStatus(customerId, order.getRelabelOrderNo(), operatorId, request, true); } + /** 在申请行锁内执行审核及作废,保留实际作业与状态历史。 */ private RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId, RelabelRequests.ChangeStatus request, boolean directTailOrderReview) { - RelabelApplication application = StringUtils.hasText(customerId) - ? applications.lockNo(customerId, relabelNo) - : applications.lockAnyByRelabelNo(relabelNo); + RelabelApplication application = statusRepository.lockApplication(customerId, relabelNo); if (application == null) throw notFound("Relabel application not found"); customerId = application.getCustomerId(); String next = request.status().trim().toUpperCase(); @@ -652,19 +653,20 @@ public class RelabelApplicationService implements WalletResultHandler { if ("REJECTED".equals(next) && !StringUtils.hasText(request.remark())) { throw badRequest("审核驳回必须填写原因"); } - LastlegOrder primaryOrder = orders.selectById(application.getLastlegOrderId()); + LastlegOrder primaryOrder = statusRepository.findPrimaryOrder(application.getLastlegOrderId()); boolean directApplication = primaryOrder != null && "RELABEL_APPLICATION".equals(primaryOrder.getOrderType()); - if (PENDING_REVIEW.equals(application.getStatus()) && directApplication != directTailOrderReview) { + if (!"CANCELLED".equals(next) && PENDING_REVIEW.equals(application.getStatus()) && directApplication != directTailOrderReview) { throw conflict(directApplication ? "直接创建的贴标单请在尾程订单审核" : "关联业务产生的换标单请在换标单审核"); } if (next.equals(application.getStatus())) return detail(customerId, relabelNo); validateTransition(application.getStatus(), next); - if (Set.of("REJECTED", "CANCELLED").contains(next) && itemList(application.getId()).stream() + // 作废仅释放申请占用,已经发生的换标事实继续保留;驳回仍禁止回退实际作业。 + if ("REJECTED".equals(next) && itemList(application.getId()).stream() .filter(item -> item.getReturnCartonId() != null) - .anyMatch(item -> returnCartons.totalRelabelledQuantity(item.getReturnCartonId()) > 0)) - throw conflict("该退货任务已有实际商品换标事实,不能取消并恢复为未换标库存;请按异常完工或退回重做处理"); + .anyMatch(item -> statusRepository.relabelledQuantity(item.getReturnCartonId()) > 0)) + throw conflict("该退货任务已有实际商品换标事实,不能驳回并恢复为未换标库存;请按异常完工或退回重做处理"); if (PENDING_REVIEW.equals(application.getStatus()) && PENDING_PROCESSING.equals(next) && itemList(application.getId()).stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(operationType(item)))) { boolean hasCartonFile = StringUtils.hasText(application.getCartonTotalFileId()); @@ -679,20 +681,20 @@ public class RelabelApplicationService implements WalletResultHandler { LocalDateTime now = LocalDateTime.now(); application.setStatus(next); application.setUpdatedAt(now); - applications.updateById(application); + statusRepository.saveApplication(application); List linkedOrders = tailOrders(application); if (linkedOrders.isEmpty()) throw conflict("Linked tail-leg order is missing"); if (PENDING_PROCESSING.equals(next)) { application.setFulfillmentStatus("WAIT_RELABEL"); - applications.updateById(application); + statusRepository.saveApplication(application); } if (CUSTOMER_CONFIRM_PENDING.equals(next)) { application.setFulfillmentStatus("WAIT_CUSTOMER_CONFIRMATION"); - applications.updateById(application); + statusRepository.saveApplication(application); } else if ("COMPLETED".equals(next)) { boolean preDispatched = "PRE_DISPATCH_CREATED".equals(application.getFulfillmentStatus()); application.setFulfillmentStatus("OUTBOUNDED"); - applications.updateById(application); + statusRepository.saveApplication(application); if (!StringUtils.hasText(application.getOutboundOrderNo()) && !preDispatched) { releaseInventory(customerId, operatorId, application); @@ -703,7 +705,7 @@ public class RelabelApplicationService implements WalletResultHandler { if ("FROZEN".equals(application.getWalletStatus())) { walletCommands.publish("RELEASE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null, "换标取消,释放冻结", 2L); - application.setWalletStatus("RELEASE_PENDING"); applications.updateById(application); + application.setWalletStatus("RELEASE_PENDING"); statusRepository.saveApplication(application); } } for (LastlegOrder order : linkedOrders) { @@ -719,27 +721,25 @@ public class RelabelApplicationService implements WalletResultHandler { case "CANCELLED" -> "CANCELLED"; default -> mainStatus; }; - // Reviewing/processing a return relabel must not move the already - // completed inbound order back to assignment or relabel waiting. - if ("RETURN_RELABEL".equals(order.getOrderType())) { + // 退货不回退已完成入库;转运作废只终止换标,不取消原转运主单。 + if ("RETURN_RELABEL".equals(order.getOrderType()) + || "WAREHOUSE_TRANSFER".equals(order.getOrderType()) && "CANCELLED".equals(next)) { orderStatus = orderBefore; } order.setCurrentStatus(orderStatus); if (!"RETURN_RELABEL".equals(order.getOrderType())) order.setSubOrderStatus("RELABEL:" + next); order.setRelabelStatus(businessRelabelStatus(next)); order.setUpdatedAt(now); - orders.updateById(order); + statusRepository.saveOrder(order); if (!Objects.equals(orderBefore, orderStatus)) { insertOrderHistory(customerId, order.getId(), orderBefore, orderStatus, request.remark(), now); } } - List linkedReturnOrders = returnOrders.selectList(new LambdaQueryWrapper() - .eq(ReturnOrder::getRelabelApplicationId, application.getId()) - .eq(ReturnOrder::getCustomerId, customerId)); + List linkedReturnOrders = statusRepository.linkedReturnOrders(customerId, application.getId()); for (ReturnOrder returnOrder : linkedReturnOrders) { returnOrder.setRelabelStatus(next); returnOrder.setUpdatedAt(now); - returnOrders.updateById(returnOrder); + statusRepository.saveReturnOrder(returnOrder); } insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now); return detail(customerId, relabelNo); @@ -1535,7 +1535,12 @@ public class RelabelApplicationService implements WalletResultHandler { value.setToStatus(to); value.setEventId("RELABEL_APPLICATION"); value.setRemark(trim(remark)); value.setCreatedAt(now); orderHistories.insert(value); } + /** 客户确认换标完成后禁止作废,其余状态允许终止申请。 */ private static void validateTransition(String from, String to) { + if ("CANCELLED".equals(to)) { + if (Set.of(READY_FOR_OUTBOUND, "COMPLETED").contains(from)) throw conflict("换标完成的单据不能作废"); + return; + } boolean allowed = PENDING_REVIEW.equals(from) && Set.of(PENDING_PROCESSING, "REJECTED", "CANCELLED").contains(to) || PENDING_PROCESSING.equals(from) && Set.of("PROCESSING", "REJECTED", "CANCELLED").contains(to) || "PROCESSING".equals(from) && Set.of(CUSTOMER_CONFIRM_PENDING, "REJECTED", "CANCELLED").contains(to) 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 79ac571a3..21f4fc5b5 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 @@ -187,8 +187,12 @@ public final class WarehouseTransferRequests { @Size(max = 255) String productNameZh, @Positive int packageCount, @Size(max = 255) String shippingMark, - @Positive Long quantity + @Positive Long quantity, + @Size(max = 5000) List<@NotBlank @jakarta.validation.constraints.Pattern(regexp = "BX-[A-Z0-9]{1,40}") String> cartonMarks ) { + public Item(String skuCode, String productNameZh, int packageCount, String shippingMark, Long quantity) { + this(skuCode, productNameZh, packageCount, shippingMark, quantity, null); + } public Item(String skuCode, String productNameZh, int packageCount, String shippingMark) { this(skuCode, productNameZh, packageCount, shippingMark, null); } 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 6244bb1ba..6b5984cb3 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 @@ -199,6 +199,7 @@ public class WarehouseTransferService { if (itemPackages != request.packageCount()) { throw badRequest("Cargo item package total must equal transfer package count"); } + validatePrintedCartonMarks(request); validateRelabelRequest(customerId, request); validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), @@ -321,7 +322,8 @@ public class WarehouseTransferService { for (int itemCarton = 0; itemCarton < source.packageCount(); itemCarton++) { cartonIndex++; insertTransferCarton(customerId, transferId, itemId, transferNo, - cartonIndex, request.packageCount(), itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null, now); + cartonIndex, request.packageCount(), itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null, now, + source.cartonMarks() == null ? null : source.cartonMarks().get(itemCarton)); } insertOrderDetail(customerId, order.getId(), transferNo, index, source, request, now); } @@ -352,6 +354,7 @@ public class WarehouseTransferService { if (itemPackages != request.packageCount()) { throw badRequest("Cargo item package total must equal transfer package count"); } + validatePrintedCartonMarks(request); validateRelabelRequest(customerId, request); validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), @@ -439,8 +442,9 @@ public class WarehouseTransferService { String customerMark = itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null; if (existingCarton == null) { insertTransferCarton(customerId, transfer.getId(), itemId, transferNo, cartonIndex, - request.packageCount(), customerMark, now); + request.packageCount(), customerMark, now, source.cartonMarks() == null ? null : source.cartonMarks().get(itemCarton)); } else { + if (source.cartonMarks() != null && !existingCarton.getCartonMark().equals(source.cartonMarks().get(itemCarton))) throw conflict("已保存箱唛不能替换,请重新读取草稿"); existingCarton.setTransferItemId(itemId); existingCarton.setTotalCartons(request.packageCount()); existingCarton.setCustomerCartonMark(customerMark); existingCarton.setUpdatedAt(now); transferCartons.updateById(existingCarton); @@ -2134,6 +2138,17 @@ public class WarehouseTransferService { return order; } + private void validatePrintedCartonMarks(WarehouseTransferRequests.Create request) { + Set marks = new java.util.HashSet<>(); + for (var item : request.items()) { + if (item.cartonMarks() == null) continue; + if (item.cartonMarks().size() != item.packageCount()) throw badRequest("已生成箱唛数量必须等于预报箱数,请重新打印"); + for (String mark : item.cartonMarks()) { + if (mark == null || !mark.matches("BX-[A-Z0-9]{1,40}") || !marks.add(mark)) throw badRequest("已生成箱唛格式错误或重复,请重新打印"); + } + } + } + private long insertTransferItem(String customerId, long transferId, WarehouseTransferRequests.Item source, LocalDateTime now) { WarehouseTransferItem item = new WarehouseTransferItem(); @@ -2148,10 +2163,10 @@ public class WarehouseTransferService { private void insertTransferCarton(String customerId, long transferId, long transferItemId, String transferNo, int cartonIndex, int totalCartons, - String customerCartonMark, LocalDateTime now) { + String customerCartonMark, LocalDateTime now, String printedMark) { WarehouseTransferCarton carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId()); carton.setTransferOrderId(transferId); carton.setTransferItemId(transferItemId); - carton.setCustomerId(customerId); carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId())); + carton.setCustomerId(customerId); carton.setCartonMark(printedMark == null ? LastlegOrderNoGenerator.compactCartonMark(carton.getId()) : printedMark); carton.setCustomerCartonMark(trim(customerCartonMark)); carton.setCartonIndex(cartonIndex); carton.setTotalCartons(totalCartons); carton.setStatus("EXPECTED"); carton.setCreatedAt(now); carton.setUpdatedAt(now); transferCartons.insert(carton); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java index 16bba7407..1e1740d23 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java @@ -230,6 +230,11 @@ class RelabelApplicationServiceTest { }); lenient().when(pdaTasks.updateWithVersion(any(), any())).thenReturn(1); ReflectionTestUtils.setField(service, "applications", applications); + var statusRepository = new com.baiyue.lastleg.relabel.repository.RelabelStatusRepository(); + ReflectionTestUtils.setField(statusRepository, "applications", applications); + ReflectionTestUtils.setField(statusRepository, "orders", orders); + ReflectionTestUtils.setField(statusRepository, "returnOrders", returnOrders); + ReflectionTestUtils.setField(service, "statusRepository", statusRepository); ReflectionTestUtils.setField(service, "transferRelabelCompletion", transferRelabelCompletion); ReflectionTestUtils.setField(service, "items", items); ReflectionTestUtils.setField(service, "histories", histories); @@ -1305,6 +1310,58 @@ class RelabelApplicationServiceTest { verify(applications, never()).insert(any(RelabelApplication.class)); } + @org.junit.jupiter.params.ParameterizedTest + @org.junit.jupiter.params.provider.ValueSource(strings = {"PENDING_REVIEW", "PENDING_PROCESSING", "PROCESSING", "CUSTOMER_CONFIRM_PENDING", "REJECTED"}) + void unfinishedApplicationsCanBeVoidedAndRetainTransferMainStatus(String status) { + var app = application(11L, "RL-VOID", "10001", "void-key", "hash", status); + app.setLastlegOrderId(21L); app.setWalletStatus("FROZEN"); + when(applications.lockNo("10001", "RL-VOID")).thenReturn(app); + stubDetail(app); + var tail = order(21L, "10001", "WT-1", "INBOUND_COMPLETED"); tail.setOrderType("WAREHOUSE_TRANSFER"); + when(orders.selectById(21L)).thenReturn(tail); + service.changeStatus("10001", "RL-VOID", "operator", new RelabelRequests.ChangeStatus("CANCELLED", "废弃预填换标要求")); + assertEquals("CANCELLED", app.getStatus()); + assertEquals("INBOUND_COMPLETED", tail.getCurrentStatus()); + assertEquals("CANCELLED", tail.getRelabelStatus()); + assertEquals("RELEASE_PENDING", app.getWalletStatus()); + verify(walletCommands).publish(eq("RELEASE"), eq(10001L), eq("RELABEL_ORDER"), eq("RL-VOID"), isNull(), anyString(), eq(2L)); + verify(histories).insert(any(RelabelStatusHistory.class)); + } + + @Test void repeatedVoidReturnsExistingRecordWithoutReleasingAgain() { + var app = application(11L, "RL-VOID-AGAIN", "10001", "key", "hash", "CANCELLED"); + app.setLastlegOrderId(21L); + when(applications.lockNo("10001", app.getRelabelNo())).thenReturn(app); stubDetail(app); + var result = service.changeStatus("10001", app.getRelabelNo(), "operator", new RelabelRequests.ChangeStatus("CANCELLED", "作废")); + assertEquals("CANCELLED", result.application().status()); + verify(applications, never()).updateById(any(RelabelApplication.class)); + verify(inventoryCommands, never()).release(anyString(), anyString(), any()); + verify(histories, never()).insert(any(RelabelStatusHistory.class)); + } + + @Test void directPendingReviewApplicationCanBeVoidedOutsideTailOrderReview() { + var app = application(11L, "RL-DIRECT-VOID", "10001", "void-key", "hash", "PENDING_REVIEW"); + app.setLastlegOrderId(21L); + when(applications.lockNo("10001", app.getRelabelNo())).thenReturn(app); stubDetail(app); + var tail = order(21L, "10001", "RL-ORDER", "WAIT_RELABEL"); tail.setOrderType("RELABEL_APPLICATION"); + when(orders.selectById(21L)).thenReturn(tail); + service.changeStatus("10001", app.getRelabelNo(), "operator", new RelabelRequests.ChangeStatus("CANCELLED", "作废")); + assertEquals("CANCELLED", tail.getCurrentStatus()); + } + + @org.junit.jupiter.params.ParameterizedTest + @org.junit.jupiter.params.provider.ValueSource(strings = {"READY_FOR_OUTBOUND", "COMPLETED"}) + void completedRelabelCannotBeVoided(String status) { + var app = application(11L, "RL-DONE", "10001", "key", "hash", status); + app.setLastlegOrderId(21L); + when(applications.lockNo("10001", "RL-DONE")).thenReturn(app); + when(orders.selectById(21L)).thenReturn(order(21L, "10001", "ORDER", "COMPLETED")); + var failure = assertThrows(ResponseStatusException.class, () -> service.changeStatus("10001", "RL-DONE", "operator", new RelabelRequests.ChangeStatus("CANCELLED", "作废"))); + assertTrue(failure.getReason().contains("不能作废")); + verify(applications, never()).updateById(any(RelabelApplication.class)); + verify(walletCommands, never()).publish(anyString(), anyLong(), anyString(), anyString(), any(), any(), anyLong()); + } + private void stubDetail(RelabelApplication application) { when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application); when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6))); 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 6d04a7b3c..8ea7bbb47 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 @@ -619,7 +619,7 @@ class WarehouseTransferServiceTest { new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0), null, null, "TRACK-MUST-NOT-BE-SAVED", null, "Incoming transit cargo", true, null, null, null, null, null, "key-incoming", - List.of(new WarehouseTransferRequests.Item("SKU-1", "Product A", 2, null), + List.of(new WarehouseTransferRequests.Item("SKU-1", "Product A", 2, null, null, List.of("BX-PRINTED1", "BX-PRINTED2")), new WarehouseTransferRequests.Item("SKU-2", "Product B", 3, null)), null); when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT)); when(transferItems.selectList(any())).thenReturn(List.of()); @@ -647,6 +647,8 @@ class WarehouseTransferServiceTest { assertEquals("PENDING_BINDING", tailOrder.getValue().getRelabelStatus()); assertEquals(List.of(1, 2, 3, 4, 5), cartons.getAllValues().stream() .map(WarehouseTransferCarton::getCartonIndex).toList()); + assertEquals(List.of("BX-PRINTED1", "BX-PRINTED2"), cartons.getAllValues().subList(0, 2).stream() + .map(WarehouseTransferCarton::getCartonMark).toList()); assertTrue(cartons.getAllValues().stream().allMatch(value -> value.getCartonMark() .matches("BX-[0-9A-Z]+"))); when(transfers.findKey("TENANT-A", request.idempotencyKey())).thenReturn(transfer.getValue()); @@ -814,7 +816,20 @@ class WarehouseTransferServiceTest { verify(transfers, never()).updateById(any(WarehouseTransferOrder.class)); } - /** 草稿预先打印与后续正式提交使用同一服务端箱唛。 */ + /** 未建单时生成的箱唛必须完整且不重复。 */ + @Test + void duplicateOrMismatchedPreprintedMarksRejectBeforeCreatingOrder() throws Exception { + var mapper = new ObjectMapper().findAndRegisterModules(); + for (var marks : List.of(List.of("BX-ONE"), List.of("BX-ONE", "BX-ONE", "BX-3", "BX-4", "BX-5"))) { + com.fasterxml.jackson.databind.node.ObjectNode json = mapper.valueToTree(request(false)); + json.remove("inboundModeValid"); + ((com.fasterxml.jackson.databind.node.ObjectNode) json.get("items").get(0)).set("cartonMarks", mapper.valueToTree(marks)); + var input = mapper.treeToValue(json, WarehouseTransferRequests.Create.class); + assertThrows(ResponseStatusException.class, () -> service.create("TENANT-A", "USER-1", input)); + } + verify(transfers, never()).insert(any(WarehouseTransferOrder.class)); + } + @Test void draftCanPrintPersistedCartonLabels() { WarehouseTransferOrder transfer = persistedTransfer(); transfer.setStatus(WarehouseTransferService.DRAFT);