From ce5144d27ed320041944a914c82b135c28d60de3 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 19 Aug 2026 15:52:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lastleg/AGENTS.md | 2 + .../service/DeliveryTransferService.java | 1 + .../controller/DispatchController.java | 10 +++ .../pda/controller/DispatchPdaController.java | 10 +++ .../dispatch/service/DispatchService.java | 9 +-- .../lastleg/dto/LastlegOrderSummary.java | 1 + .../baiyue/lastleg/entity/LastlegOrder.java | 1 + .../pda/controller/InboundPdaController.java | 13 ++++ .../service/InboundStockReceiptService.java | 27 +++++++ .../InventoryCommandController.java | 16 +++++ .../service/RelabelApplicationService.java | 19 +++-- .../lastleg/service/LastlegOrderService.java | 6 +- .../WarehouseTransferController.java | 11 +++ .../service/WarehouseTransferService.java | 27 ++++--- lastleg/src/main/resources/db/lastleg.sql | 1 + .../dispatch/service/DispatchServiceTest.java | 19 +++++ .../InboundStockReceiptServiceTest.java | 9 +++ .../InventoryCommandControllerTest.java | 72 +++++++++++++++++++ .../RelabelApplicationServiceTest.java | 8 ++- .../service/WarehouseTransferServiceTest.java | 5 +- scripts/db/incremental.sql | 6 ++ 21 files changed, 243 insertions(+), 30 deletions(-) create mode 100644 lastleg/src/test/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandControllerTest.java diff --git a/lastleg/AGENTS.md b/lastleg/AGENTS.md index f39b967..7a71049 100644 --- a/lastleg/AGENTS.md +++ b/lastleg/AGENTS.md @@ -61,6 +61,8 @@ - 目的仓变更必须写状态/操作历史或审计证据,并使用版本控制防止并发覆盖。 - 货物废弃只使用可核对 SKU 和箱唛的一件代发库存。创建申请只能将箱从 `AVAILABLE` 锁定为 `DISCARD_PENDING`,不得扣减 SKU 箱数;实际扣库存节点固定为具备 `lastleg:cargo-discard:approve` 权限的仓库/PDA 审核人确认废弃时,并且必须在审核事务内调用专用 discard adjustment。该 adjustment 必须将箱改为 `DISCARDED`、写逐箱业务流水及唯一幂等键,并校验 `扣减前箱数 - 废弃箱数 = 扣减后箱数` 且逐箱数等于申请总箱数;任一环节失败须整体回滚。拒绝申请只释放锁箱,不得扣库存。 - 海外采购客户下单固定进入 `PENDING_REVIEW`;采购表的 `inbound_quantity - outbound_quantity` 只表示采购业务单据余量,不得写入或冒充 `available_packages`。审核/下单只能停留在采购单据(或明确的 `procurement_packages` 桶);只有海外仓 PDA 完成实收后,`InboundStockReceiptService` 才能以 `PDA_STOCK_IN:{taskId}:{allocationId}` 幂等键调用库存 `stockIn` 转入可用库存。禁止通过通用平台接口绕过 PDA 入可用库存。 +- 派车状态只允许 PDA 拣货、完成和出库动作推进,普通派车编辑接口不得修改状态。PDA 写操作要求 `lastleg:pda:operate`;后台库存表单操作要求 `lastleg:inventory:manage`,盘点调整另要求 `lastleg:inventory:adjust`,转仓库存操作要求 `lastleg:warehouse-transfer:manage`。 +- `ll_order.current_status` 只保存尾程主流程状态;换标、转仓、出库和派送等子单精确状态保存到各自单据并汇总到 `sub_order_status`,不得再用子单状态覆盖主流程状态。PDA 入仓及库存落账完成后,主流程状态固定推进为 `INBOUND_COMPLETED`。 ## 修改与验证 diff --git a/lastleg/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java b/lastleg/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java index 4b1f56f..b7e3354 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java @@ -185,6 +185,7 @@ public class DeliveryTransferService { .eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, orderIds))) { String from = order.getCurrentStatus(); order.setOutboundOrderNo(outboundNo); order.setDeliveryOrderNo(deliveryNo); order.setCurrentStatus("WAIT_OUTBOUND"); order.setUpdatedAt(now); + order.setSubOrderStatus("OUTBOUND:WAIT_OUTBOUND;DELIVERY:WAIT_DELIVERY"); tailOrders.updateById(order); LastlegOrderStatusHistory history = new LastlegOrderStatusHistory(); history.setOrderId(order.getId()); history.setFromStatus(from); history.setToStatus("WAIT_OUTBOUND"); diff --git a/lastleg/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java b/lastleg/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java index b32a99b..9cf6b7f 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java @@ -9,6 +9,7 @@ import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests; import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses; import com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService; +import com.baiyue.lastleg.dispatch.pda.controller.DispatchPdaController; import com.baiyue.lastleg.dispatch.service.DispatchService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -103,6 +104,7 @@ public class DispatchController { @RequestParam(required = false) String customerId, @PathVariable String dispatchNo, @Valid @RequestBody DispatchPdaRequests.Depart request) { + requirePdaOperate(user); String scopedTenant = scope(user, customerId); String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId()); return ApiResponse.success(pdaWorkflow.depart(scopedTenant, dispatchNo, operatorId, request)); @@ -121,4 +123,12 @@ public class DispatchController { } return String.valueOf(user.tenantId()); } + + private void requirePdaOperate(AuthenticatedUser user) { + if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + if (user.permissions() == null || !user.permissions().contains(DispatchPdaController.OPERATE_PERMISSION)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, + DispatchPdaController.OPERATE_PERMISSION + " permission is required"); + } + } } diff --git a/lastleg/src/main/java/com/baiyue/lastleg/dispatch/pda/controller/DispatchPdaController.java b/lastleg/src/main/java/com/baiyue/lastleg/dispatch/pda/controller/DispatchPdaController.java index 7c4ace5..e45211b 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/dispatch/pda/controller/DispatchPdaController.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/dispatch/pda/controller/DispatchPdaController.java @@ -15,6 +15,7 @@ import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/platform/v1/last-leg/dispatches/{dispatchNo}/pda") public class DispatchPdaController { + public static final String OPERATE_PERMISSION = "lastleg:pda:operate"; @Resource private DispatchPdaWorkflowService service; @PostMapping("/push") @@ -22,6 +23,7 @@ public class DispatchPdaController { @PathVariable String dispatchNo, @RequestParam(required = false) String customerId, @Valid @RequestBody DispatchPdaRequests.Push request) { + requireOperate(user); return ApiResponse.success(service.push(scope(user, customerId), dispatchNo, request)); } @GetMapping @@ -35,6 +37,7 @@ public class DispatchPdaController { @PathVariable String dispatchNo, @PathVariable long itemId, @RequestParam(required = false) String customerId, @Valid @RequestBody DispatchPdaRequests.Pick request) { + requireOperate(user); return ApiResponse.success(service.pick(scope(user, customerId), dispatchNo, itemId, operator(user), request)); } @PostMapping("/complete") @@ -43,6 +46,7 @@ public class DispatchPdaController { @PathVariable String dispatchNo, @RequestParam(required = false) String customerId, @Valid @RequestBody DispatchPdaRequests.CompletePicking request) { + requireOperate(user); return ApiResponse.success(service.completePicking(scope(user, customerId), dispatchNo, request)); } private String scope(AuthenticatedUser user, String requestedCustomer) { @@ -56,4 +60,10 @@ public class DispatchPdaController { return String.valueOf(user.tenantId()); } private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); } + private void requireOperate(AuthenticatedUser user) { + if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + if (user.permissions() == null || !user.permissions().contains(OPERATE_PERMISSION)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, OPERATE_PERMISSION + " permission is required"); + } + } } diff --git a/lastleg/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index 42abf0c..bd389d8 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -35,8 +35,6 @@ import java.util.stream.Collectors; public class DispatchService { public static final String PENDING_DEPARTURE = "PENDING_DEPARTURE"; public static final String CANCELLED = "CANCELLED"; - private static final Set STATUSES = Set.of(PENDING_DEPARTURE, "PICKING", "READY_TO_DEPART", - "PARTIAL_READY_TO_DEPART", "DEPARTED", "PARTIAL_DEPARTED", "COMPLETED", CANCELLED); @Resource private DispatchOrderMapper orders; @Resource private DispatchItemMapper items; @@ -185,11 +183,8 @@ public class DispatchService { dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus())); dispatch.setEstimatedArrivalAt(request.estimatedArrivalAt()); if (StringUtils.hasText(request.status())) { - String status = request.status().trim().toUpperCase(); - if (!STATUSES.contains(status) || CANCELLED.equals(status)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Use cancel endpoint or a valid dispatch status"); - } - dispatch.setStatus(status); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Dispatch status is PDA-managed and cannot be changed by the edit endpoint"); } dispatch.setUpdatedAt(LocalDateTime.now()); orders.updateById(dispatch); diff --git a/lastleg/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java b/lastleg/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java index f4019cc..0ae6357 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java @@ -42,6 +42,7 @@ public record LastlegOrderSummary( String relabelStatus, String palletMergeFlag, String currentStatus, + String subOrderStatus, String privateRemark, String customerRemark, String warehouseRemark, diff --git a/lastleg/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java b/lastleg/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java index 7a87035..4337ed4 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java @@ -50,6 +50,7 @@ public class LastlegOrder { private String relabelStatus; private String palletMergeFlag; private String currentStatus; + private String subOrderStatus; private String privateRemark; private String customerRemark; private String warehouseRemark; diff --git a/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/InboundPdaController.java b/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/InboundPdaController.java index d6e75f0..7ade7f6 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/InboundPdaController.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/InboundPdaController.java @@ -23,6 +23,7 @@ import java.util.List; @RestController @RequestMapping("/api/platform/v1/last-leg/inbound-plans/{planNo}/pda") public class InboundPdaController { + public static final String OPERATE_PERMISSION = "lastleg:pda:operate"; @Resource private InboundPdaWorkflowService service; @PostMapping("/push") @@ -30,6 +31,7 @@ public class InboundPdaController { @PathVariable String planNo, @RequestParam(required = false) String customerId, @Valid @RequestBody PdaRequests.Push request) { + requireOperate(user); return ApiResponse.success(service.push(scope(user, customerId), planNo, request)); } @@ -46,6 +48,7 @@ public class InboundPdaController { @PathVariable long allocationId, @RequestParam(required = false) String customerId, @Valid @RequestBody PdaRequests.PackageOperation request) { + requireOperate(user); return ApiResponse.success(service.count(scope(user, customerId), planNo, allocationId, request)); } @@ -55,6 +58,7 @@ public class InboundPdaController { @PathVariable long allocationId, @RequestParam(required = false) String customerId, @Valid @RequestBody PdaRequests.PackageOperation request) { + requireOperate(user); return ApiResponse.success(service.pick(scope(user, customerId), planNo, allocationId, request)); } @@ -71,6 +75,7 @@ public class InboundPdaController { @PathVariable long allocationId, @RequestParam(required = false) String customerId, @Valid @RequestBody PdaRequests.ConfirmMerge request) { + requireOperate(user); return ApiResponse.success(service.merge(scope(user, customerId), planNo, allocationId, request)); } @@ -78,6 +83,7 @@ public class InboundPdaController { public ApiResponse complete(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo, @RequestParam(required = false) String customerId) { + requireOperate(user); return ApiResponse.success(service.complete(scope(user, customerId), planNo)); } @@ -91,4 +97,11 @@ public class InboundPdaController { } return String.valueOf(user.tenantId()); } + + private void requireOperate(AuthenticatedUser user) { + if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + if (user.permissions() == null || !user.permissions().contains(OPERATE_PERMISSION)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, OPERATE_PERMISSION + " permission is required"); + } + } } diff --git a/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptService.java b/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptService.java index 62b6d50..a2ec49f 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptService.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptService.java @@ -13,6 +13,8 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; +import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; @@ -35,6 +37,7 @@ public class InboundStockReceiptService { @Resource private InboundStockReceiptMapper receipts; @Resource private InventoryCommandService inventory; @Resource private DropshipOrderService dropshipOrders; + @Resource private LastlegOrderStatusHistoryMapper orderHistories; @Transactional public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task, @@ -59,6 +62,7 @@ public class InboundStockReceiptService { .filter(Objects::nonNull).collect(Collectors.toSet()); if (receivedAllocationIds.equals(expectedAllocationIds)) { activateDropship(customerId, task.getId(), orderById, existing); + completeOrders(customerId, plan, orderById, receivedAt); return; } throw conflict("PDA stock-in receipts are incomplete; repair the task receipts before retrying"); @@ -81,6 +85,29 @@ public class InboundStockReceiptService { } } activateDropship(customerId, task.getId(), orderById, created); + completeOrders(customerId, plan, orderById, receivedAt); + } + + private void completeOrders(String customerId, InboundPlan plan, Map orderById, + LocalDateTime completedAt) { + for (LastlegOrder snapshot : orderById.values()) { + LastlegOrder order = orders.lockById(customerId, snapshot.getId()); + if (order == null) throw conflict("Inbound tail-leg order disappeared while completing stock-in"); + if ("INBOUND_COMPLETED".equals(order.getCurrentStatus())) continue; + String before = order.getCurrentStatus(); + order.setCurrentStatus("INBOUND_COMPLETED"); + order.setSubOrderStatus("INBOUND_PLAN:COMPLETED"); + order.setUpdatedAt(completedAt); + orders.updateById(order); + LastlegOrderStatusHistory history = new LastlegOrderStatusHistory(); + history.setOrderId(order.getId()); + history.setFromStatus(before); + history.setToStatus("INBOUND_COMPLETED"); + history.setEventId("PDA_INBOUND_COMPLETE:" + plan.getPlanNo()); + history.setRemark("PDA inbound completed and inventory stocked in"); + history.setCreatedAt(completedAt); + orderHistories.insert(history); + } } private InboundStockReceipt persist(String customerId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line, diff --git a/lastleg/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java b/lastleg/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java index 9409e34..71d44c8 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java @@ -21,12 +21,15 @@ import org.springframework.web.server.ResponseStatusException; @RequestMapping("/api/platform/v1/last-leg/inventory") @Tag(name = "Tail-leg inbound-order package inventory commands") public class InventoryCommandController { + public static final String MANAGE_PERMISSION = "lastleg:inventory:manage"; + public static final String ADJUST_PERMISSION = "lastleg:inventory:adjust"; @Resource private InventoryCommandService service; @PostMapping("/hold") @Operation(summary = "Move available inbound-order packages to hold") public ApiResponse hold(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.hold(scope(user, request.customerId()), operator(user), request)); } @@ -34,6 +37,7 @@ public class InventoryCommandController { @Operation(summary = "Release held inbound-order packages") public ApiResponse release(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.release(scope(user, request.customerId()), operator(user), request)); } @@ -41,6 +45,7 @@ public class InventoryCommandController { @Operation(summary = "Allocate available inbound-order packages to an outbound order") public ApiResponse allocate(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.allocate(scope(user, request.customerId()), operator(user), request)); } @@ -48,6 +53,7 @@ public class InventoryCommandController { @Operation(summary = "Cancel an outbound allocation") public ApiResponse unallocate(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.unallocate(scope(user, request.customerId()), operator(user), request)); } @@ -55,6 +61,7 @@ public class InventoryCommandController { @Operation(summary = "Confirm picked inbound-order packages") public ApiResponse pick(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.pick(scope(user, request.customerId()), operator(user), request)); } @@ -62,6 +69,7 @@ public class InventoryCommandController { @Operation(summary = "Ship picked inbound-order packages and report whether the pallet is empty") public ApiResponse ship(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Move request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.ship(scope(user, request.customerId()), operator(user), request)); } @@ -69,6 +77,7 @@ public class InventoryCommandController { @Operation(summary = "Adjust inbound-order package inventory after a physical count") public ApiResponse adjust(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody InventoryCommandRequests.Adjustment request) { + requirePermission(user, ADJUST_PERMISSION); return ApiResponse.success(service.adjust(scope(user, request.customerId()), operator(user), request)); } @@ -87,4 +96,11 @@ public class InventoryCommandController { private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); } + + private void requirePermission(AuthenticatedUser user, String permission) { + if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + if (user.permissions() == null || !user.permissions().contains(permission)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, permission + " permission is required"); + } + } } diff --git a/lastleg/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index 935dc14..8a3fe2e 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -82,7 +82,8 @@ public class RelabelApplicationService { order.setPackageCount(total); order.setPickedPackageCount(0); order.setShippedPackageCount(0); - order.setCurrentStatus(PENDING_PROCESSING); + order.setCurrentStatus("PENDING_ASSIGNMENT"); + order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING); order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_PROCESSING); order.setCustomerRemark(trim(request.remark())); @@ -117,7 +118,7 @@ public class RelabelApplicationService { for (RelabelRequests.Item source : request.items()) insertItem(customerId, applicationId, source, now); insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now); - insertOrderHistory(order.getId(), null, PENDING_PROCESSING, "Relabel application submitted", now); + insertOrderHistory(order.getId(), null, order.getCurrentStatus(), "Relabel application submitted", now); return detail(customerId, relabelNo); } @@ -174,15 +175,23 @@ public class RelabelApplicationService { LastlegOrder order = orders.selectById(application.getLastlegOrderId()); if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing"); String orderBefore = order.getCurrentStatus(); + String mainStatus = STATUSES.contains(orderBefore) ? "PENDING_ASSIGNMENT" : orderBefore; // A return-relabel tail order joins the normal inbound queue after labels are finished. - String orderStatus = "COMPLETED".equals(next) && "RETURN_RELABEL".equals(order.getOrderType()) - ? "PENDING_ASSIGNMENT" : next; + String orderStatus = switch (next) { + case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType()) + ? "PENDING_ASSIGNMENT" : "COMPLETED"; + case "REJECTED", "CANCELLED" -> "CANCELLED"; + default -> mainStatus; + }; order.setCurrentStatus(orderStatus); + order.setSubOrderStatus("RELABEL:" + next); order.setRelabelStatus(next); order.setUpdatedAt(now); orders.updateById(order); insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now); - insertOrderHistory(order.getId(), orderBefore, orderStatus, request.remark(), now); + if (!Objects.equals(orderBefore, orderStatus)) { + insertOrderHistory(order.getId(), orderBefore, orderStatus, request.remark(), now); + } return detail(customerId, relabelNo); } diff --git a/lastleg/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java b/lastleg/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java index 3870d14..414d206 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java @@ -126,7 +126,7 @@ public class LastlegOrderService { List orders = orderMapper.selectList(filter(customerId, query) .orderByDesc(LastlegOrder::getCreatedAt).last("LIMIT 10000")); List lines = new ArrayList<>(); - lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,已拣箱数,已出库箱数,剩余箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,出库单号,派送单号,换标单号,换标状态,合托标记,当前状态,备注(仅自己),备注(客户),备注(仓库)"); + lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,已拣箱数,已出库箱数,剩余箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,出库单号,派送单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)"); for (LastlegOrder order : orders) { lines.add(String.join(",", List.of( csv(order.getId()), csv(order.getOrderNo()), csv(order.getOrderType()), csv(order.getInboundCode()), @@ -140,7 +140,7 @@ public class LastlegOrderService { csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()), csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getOutboundOrderNo()), csv(order.getDeliveryOrderNo()), csv(order.getRelabelOrderNo()), csv(order.getRelabelStatus()), - csv(order.getPalletMergeFlag()), csv(order.getCurrentStatus()), csv(order.getPrivateRemark()), + csv(order.getPalletMergeFlag()), csv(order.getCurrentStatus()), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()), csv(order.getCustomerRemark()), csv(order.getWarehouseRemark())))); } return ("\uFEFF" + String.join("\r\n", lines)).getBytes(StandardCharsets.UTF_8); @@ -189,7 +189,7 @@ public class LastlegOrderService { - Objects.requireNonNullElse(o.getShippedPackageCount(), 0)), o.getVolumeCbm(), o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(), o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getOutboundOrderNo(), o.getDeliveryOrderNo(), - o.getRelabelOrderNo(), o.getRelabelStatus(), o.getPalletMergeFlag(), o.getCurrentStatus(), + o.getRelabelOrderNo(), o.getRelabelStatus(), o.getPalletMergeFlag(), o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(), o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt()); } diff --git a/lastleg/src/main/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferController.java b/lastleg/src/main/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferController.java index 68c9112..6165b04 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferController.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferController.java @@ -15,6 +15,7 @@ import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/platform/v1/last-leg/warehouse-transfers") public class WarehouseTransferController { + public static final String MANAGE_PERMISSION = "lastleg:warehouse-transfer:manage"; @Resource private WarehouseTransferService service; @PostMapping @@ -41,6 +42,7 @@ public class WarehouseTransferController { public ApiResponse allocate(@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @PathVariable String transferNo, @Valid @RequestBody WarehouseTransferRequests.Allocate request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.allocate(scope(user, customerId), transferNo, operator(user), request)); } @@ -48,6 +50,7 @@ public class WarehouseTransferController { public ApiResponse outbound(@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @PathVariable String transferNo, @Valid @RequestBody WarehouseTransferRequests.Outbound request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.outbound(scope(user, customerId), transferNo, operator(user), request)); } @@ -55,6 +58,7 @@ public class WarehouseTransferController { public ApiResponse receive(@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId, @PathVariable String transferNo, @Valid @RequestBody WarehouseTransferRequests.Receipt request) { + requirePermission(user, MANAGE_PERMISSION); return ApiResponse.success(service.receive(scope(user, customerId), transferNo, operator(user), request)); } @@ -65,6 +69,13 @@ public class WarehouseTransferController { } } + private void requirePermission(AuthenticatedUser user, String permission) { + if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + if (user.permissions() == null || !user.permissions().contains(permission)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, permission + " permission is required"); + } + } + private String scope(AuthenticatedUser user, String requested) { if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { diff --git a/lastleg/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index 4c34422..5abcb23 100644 --- a/lastleg/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -225,12 +225,14 @@ public class WarehouseTransferService { LocalDateTime now = LocalDateTime.now(); String before = order.getCurrentStatus(); transfer.setStatus(next); transfer.setUpdatedAt(now); transfers.updateById(transfer); - String orderStatus = APPROVED.equals(next) - ? (Boolean.TRUE.equals(transfer.getNeedRelabel()) ? RelabelApplicationService.PENDING_PROCESSING : PENDING_ASSIGNMENT) - : "CANCELLED"; - order.setCurrentStatus(orderStatus); order.setUpdatedAt(now); orders.updateById(order); - insertOrderHistory(order.getId(), before, orderStatus, transferNo, - "Warehouse transfer reviewed: " + next, now); + String orderStatus = APPROVED.equals(next) ? PENDING_ASSIGNMENT : "CANCELLED"; + order.setCurrentStatus(orderStatus); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + next); + order.setUpdatedAt(now); orders.updateById(order); + if (!java.util.Objects.equals(before, orderStatus)) { + insertOrderHistory(order.getId(), before, orderStatus, transferNo, + "Warehouse transfer reviewed: " + next, now); + } return detail(customerId, transferNo); } @@ -304,7 +306,8 @@ public class WarehouseTransferService { transfer.setStatus(ALLOCATED); transfer.setAllocationIdempotencyKey(key); transfer.setAllocationRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer); LastlegOrder order = requireTailOrder(customerId, transfer); - String before = order.getCurrentStatus(); order.setCurrentStatus("READY_TO_OUTBOUND"); order.setUpdatedAt(now); + String before = order.getCurrentStatus(); order.setCurrentStatus("WAIT_OUTBOUND"); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:ALLOCATED"); order.setUpdatedAt(now); orders.updateById(order); insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo, "Warehouse transfer inventory allocated", now); @@ -354,7 +357,8 @@ public class WarehouseTransferService { transfer.setOutboundRequestHash(requestHash); transfer.setOutboundAt(now); transfer.setUpdatedAt(now); transfers.updateById(transfer); LastlegOrder order = requireTailOrder(customerId, transfer); - String before = order.getCurrentStatus(); order.setCurrentStatus("IN_TRANSIT"); order.setUpdatedAt(now); + String before = order.getCurrentStatus(); order.setCurrentStatus("IN_TRANSIT"); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:OUTBOUNDED"); order.setUpdatedAt(now); orders.updateById(order); insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo, "Warehouse transfer left source warehouse", now); @@ -415,7 +419,8 @@ public class WarehouseTransferService { || "RECEIVED".equals(value.getStatus())); transfer.setStatus(complete ? RECEIVED : PARTIALLY_RECEIVED); transfer.setUpdatedAt(now); transfers.updateById(transfer); LastlegOrder order = requireTailOrder(customerId, transfer); - String before = order.getCurrentStatus(); order.setCurrentStatus(complete ? RECEIVED : PARTIALLY_RECEIVED); + String before = order.getCurrentStatus(); order.setCurrentStatus(complete ? "COMPLETED" : "IN_TRANSIT"); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + (complete ? RECEIVED : PARTIALLY_RECEIVED)); order.setUpdatedAt(now); orders.updateById(order); insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo, "Warehouse transfer inventory received at destination", now); @@ -435,7 +440,8 @@ public class WarehouseTransferService { order.setContainerNo(transferNo); order.setPalletCount(0); order.setPickedPackageCount(0); order.setShippedPackageCount(0); order.setEstimatedWarehouseAt(request.estimatedArrivalAt()); - order.setCurrentStatus(PENDING_REVIEW); order.setCustomerRemark(trim(request.remark())); + order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + PENDING_REVIEW); + order.setCustomerRemark(trim(request.remark())); order.setSourceEventId("WAREHOUSE_TRANSFER:" + transferNo); order.setSourceLoadingPlanId(ORDER_TYPE); order.setSourcePayloadHash("sha256:" + requestHash); order.setCreatedAt(now); order.setUpdatedAt(now); orders.insert(order); @@ -494,6 +500,7 @@ public class WarehouseTransferService { transfer.setRelabelApplicationId(applicationId); transfer.setRelabelNo(relabelNo); transfer.setUpdatedAt(now); transfers.updateById(transfer); order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(RelabelApplicationService.PENDING_PROCESSING); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + PENDING_REVIEW + ";RELABEL:" + RelabelApplicationService.PENDING_PROCESSING); order.setUpdatedAt(now); orders.updateById(order); } diff --git a/lastleg/src/main/resources/db/lastleg.sql b/lastleg/src/main/resources/db/lastleg.sql index 8b583a8..6de76fa 100644 --- a/lastleg/src/main/resources/db/lastleg.sql +++ b/lastleg/src/main/resources/db/lastleg.sql @@ -56,6 +56,7 @@ CREATE TABLE IF NOT EXISTS ll_order ( relabel_status VARCHAR(32) COMMENT '换标状态', pallet_merge_flag VARCHAR(32) COMMENT '合托标记', current_status VARCHAR(32) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '当前业务状态', + sub_order_status VARCHAR(255) NULL COMMENT '子单据状态汇总', private_remark VARCHAR(1000) COMMENT '备注(仅自己)', customer_remark VARCHAR(1000) COMMENT '备注(客户)', warehouse_remark VARCHAR(1000) COMMENT '备注(仓库)', diff --git a/lastleg/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java b/lastleg/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java index c3cf584..5991a2d 100644 --- a/lastleg/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java +++ b/lastleg/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java @@ -21,6 +21,7 @@ import org.springframework.web.server.ResponseStatusException; import java.math.BigDecimal; import java.util.List; +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -85,6 +86,24 @@ class DispatchServiceTest { verify(candidates, never()).available(any(), any(), any(), any()); } + @Test + void editCannotBypassPdaManagedDispatchStatus() { + DispatchOrder dispatch = new DispatchOrder(); + dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1"); + dispatch.setStatus(DispatchService.PENDING_DEPARTURE); + when(orders.findByDispatchNo("100", "PC1")).thenReturn(dispatch); + when(orders.lockById("100", 1L)).thenReturn(dispatch); + DispatchRequests.Update request = new DispatchRequests.Update(null, null, null, null, null, + LocalDateTime.now(), null, null, null, null, "COMPLETED"); + + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> service.update("100", "PC1", request)); + + assertEquals(400, exception.getStatusCode().value()); + assertEquals(DispatchService.PENDING_DEPARTURE, dispatch.getStatus()); + verify(orders, never()).updateById(any(DispatchOrder.class)); + } + private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) { return new DispatchPalletCandidate(1L, planPalletId, warehousePalletId, 2L, "WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1", "LIP1-P001", destination, destination, destination, destination, 12, diff --git a/lastleg/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptServiceTest.java b/lastleg/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptServiceTest.java index 805fa03..5ab60fa 100644 --- a/lastleg/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptServiceTest.java +++ b/lastleg/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptServiceTest.java @@ -13,6 +13,8 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; +import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,6 +45,7 @@ class InboundStockReceiptServiceTest { @Mock InboundStockReceiptMapper receipts; @Mock InventoryCommandService inventory; @Mock DropshipOrderService dropshipOrders; + @Mock LastlegOrderStatusHistoryMapper orderHistories; private InboundStockReceiptService service; @BeforeEach @@ -52,6 +55,7 @@ class InboundStockReceiptServiceTest { ReflectionTestUtils.setField(service, "receipts", receipts); ReflectionTestUtils.setField(service, "inventory", inventory); ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders); + ReflectionTestUtils.setField(service, "orderHistories", orderHistories); when(receipts.listTask("100", 2L)).thenReturn(List.of()); } @@ -74,6 +78,9 @@ class InboundStockReceiptServiceTest { verify(receipts).insert(receipt.capture()); assertEquals(5L, receipt.getValue().getReceivedPackages()); assertEquals(10L, receipt.getValue().getInventoryPositionId()); + assertEquals("INBOUND_COMPLETED", fixture.order.getCurrentStatus()); + assertEquals("INBOUND_PLAN:COMPLETED", fixture.order.getSubOrderStatus()); + verify(orderHistories).insert(any(LastlegOrderStatusHistory.class)); } @Test @@ -177,6 +184,8 @@ class InboundStockReceiptServiceTest { LastlegOrder order = new LastlegOrder(); order.setId(101L); order.setCustomerId("100"); order.setOrderNo("LMO-1"); order.setCustomerName("Customer"); order.setOverseasWarehouseId("OW-1"); order.setOverseasWarehouseName("Warehouse"); order.setSourceEventId("evt-1"); order.setFbaWarehouseCode("ONT8"); + order.setCurrentStatus("INBOUND_PLANNED"); + org.mockito.Mockito.lenient().when(orders.lockById("100", 101L)).thenReturn(order); LastlegOrderDetail detail = detail(201L, "SKU-1", required, quantity); return new Fixture(plan, task, line, order, detail, allocation(31L, 12L, 21L, picked), pallet(21L, 2001L)); } diff --git a/lastleg/src/test/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandControllerTest.java b/lastleg/src/test/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandControllerTest.java new file mode 100644 index 0000000..3fa85c5 --- /dev/null +++ b/lastleg/src/test/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandControllerTest.java @@ -0,0 +1,72 @@ +package com.baiyue.lastleg.inventory.command.controller; + +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; +import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse; +import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class InventoryCommandControllerTest { + @Mock InventoryCommandService service; + private InventoryCommandController controller; + + @BeforeEach + void setUp() { + controller = new InventoryCommandController(); + ReflectionTestUtils.setField(controller, "service", service); + } + + @Test + void inventoryMutationRequiresFormPermission() { + var request = move(); + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> controller.hold(user(Set.of()), request)); + assertEquals(403, exception.getStatusCode().value()); + verify(service, never()).hold(any(), any(), any()); + } + + @Test + void permittedBackOfficeUserCanRunInventoryFormCommand() { + var request = move(); + when(service.hold("100", "7", request)).thenReturn(response()); + controller.hold(user(Set.of(InventoryCommandController.MANAGE_PERMISSION)), request); + verify(service).hold("100", "7", request); + } + + @Test + void physicalAdjustmentRequiresDedicatedPermission() { + var request = new InventoryCommandRequests.Adjustment("100", 1L, 2, "COUNT", "ORD-1", "adj-1", null); + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> controller.adjust(user(Set.of(InventoryCommandController.MANAGE_PERMISSION)), request)); + assertEquals(403, exception.getStatusCode().value()); + verify(service, never()).adjust(any(), any(), any()); + } + + private InventoryCommandRequests.Move move() { + return new InventoryCommandRequests.Move("100", 1L, 2, "ORD-1", null, null, "hold-1", null); + } + + private AuthenticatedUser user(Set permissions) { + return new AuthenticatedUser(7L, 100L, 100L, 8L, "operator", "MERCHANT", "WEB", permissions); + } + + private InventoryCommandResponse response() { + return new InventoryCommandResponse(1L, "HOLD", 2L, 3L, 0, 2, 0, 0, false, false); + } +} diff --git a/lastleg/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java b/lastleg/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java index 52496ca..97a8dad 100644 --- a/lastleg/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java +++ b/lastleg/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java @@ -88,7 +88,8 @@ class RelabelApplicationServiceTest { verify(orderHistories).insert(tailHistory.capture()); assertEquals("RELABEL_APPLICATION", tailOrder.getValue().getOrderType()); - assertEquals(RelabelApplicationService.PENDING_PROCESSING, tailOrder.getValue().getCurrentStatus()); + assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus()); + assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus()); assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion()); assertEquals("LMO-RELABEL-1", tailOrder.getValue().getOrderNo()); assertEquals(tailOrder.getValue().getInboundCode(), tailOrder.getValue().getSourceInboundId()); @@ -98,7 +99,7 @@ class RelabelApplicationServiceTest { assertEquals(6, application.getValue().getNotifiedPackageCount()); assertEquals("SKU-A", relabelItem.getValue().getSkuCode()); assertEquals("PENDING_PROCESSING", relabelHistory.getValue().getToStatus()); - assertEquals("PENDING_PROCESSING", tailHistory.getValue().getToStatus()); + assertEquals("PENDING_ASSIGNMENT", tailHistory.getValue().getToStatus()); assertEquals("FBR-1", result.application().relabelNo()); } @@ -161,7 +162,8 @@ class RelabelApplicationServiceTest { verify(applications).updateById(application); ArgumentCaptor updatedOrder = ArgumentCaptor.forClass(LastlegOrder.class); verify(orders).updateById(updatedOrder.capture()); - assertEquals("PROCESSING", updatedOrder.getValue().getCurrentStatus()); + assertEquals("PENDING_ASSIGNMENT", updatedOrder.getValue().getCurrentStatus()); + assertEquals("RELABEL:PROCESSING", updatedOrder.getValue().getSubOrderStatus()); verify(histories).insert(any(RelabelStatusHistory.class)); verify(orderHistories).insert(any(LastlegOrderStatusHistory.class)); } diff --git a/lastleg/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index 08fae60..58ee4bc 100644 --- a/lastleg/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -114,7 +114,8 @@ class WarehouseTransferServiceTest { ArgumentCaptor tailOrder = ArgumentCaptor.forClass(LastlegOrder.class); verify(orders).insert(tailOrder.capture()); assertEquals(WarehouseTransferService.RETURN_RELABEL_ORDER_TYPE, tailOrder.getValue().getOrderType()); - assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getValue().getCurrentStatus()); + assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus()); + assertEquals("WAREHOUSE_TRANSFER:PENDING_REVIEW;RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus()); assertEquals("LMO-TRANSFER-1", tailOrder.getValue().getOrderNo()); assertNull(tailOrder.getValue().getTransferWarehouseId()); verify(sourceOrders).insert(any(WarehouseTransferSourceOrder.class)); @@ -193,7 +194,7 @@ class WarehouseTransferServiceTest { assertEquals(701L, captured.getValue().getPalletId()); assertEquals(5, captured.getValue().getPackageCount()); assertEquals(WarehouseTransferService.ALLOCATED, transfer.getStatus()); - assertEquals("READY_TO_OUTBOUND", result.orderStatus()); + assertEquals("WAIT_OUTBOUND", result.orderStatus()); verifyNoInteractions(inventory); } diff --git a/scripts/db/incremental.sql b/scripts/db/incremental.sql index 85ebcb4..8253ca3 100644 --- a/scripts/db/incremental.sql +++ b/scripts/db/incremental.sql @@ -3545,3 +3545,9 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='wh_pallet_business_binding' AND column_name='created_at' AND COALESCE(column_comment,'') <> '创建时间'), 'ALTER TABLE wh_pallet_business_binding MODIFY COLUMN created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT ''创建时间''', 'SELECT 1')); PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025; +-- ============================================================================ +-- 2026-08-19 / 026 +-- Separate tail-leg main workflow status from child-document status +-- ============================================================================ +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='sub_order_status'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN sub_order_status VARCHAR(255) NULL COMMENT ''子单据状态汇总'' AFTER current_status')); +PREPARE ll_order_sub_status_026 FROM @ddl; EXECUTE ll_order_sub_status_026; DEALLOCATE PREPARE ll_order_sub_status_026;