feat: complete phase 5 overseas fulfillment contracts
This commit is contained in:
+29
-20
@@ -24,43 +24,50 @@ public class DeliveryTransferController {
|
||||
@Resource private DeliveryTransferService service;
|
||||
|
||||
@PostMapping("/dispatches/{dispatchNo}/transfer-delivery")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_TRANSFER_DELIVERY + "')")
|
||||
@Operation(summary = "Create one pending outbound order and one pending delivery order")
|
||||
public ApiResponse<DeliveryResponses.TransferResult> transfer(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String dispatchNo,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody DeliveryRequests.Transfer request) {
|
||||
return ApiResponse.success(service.transfer(scope(user, customerId), dispatchNo, operator(user), request));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.platformTransfer(dispatchNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/outbound-orders/one-click-dropship")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')")
|
||||
@Operation(summary = "Create one-click dropship outbound and push a PDA picking task")
|
||||
public ApiResponse<DeliveryResponses.PdaTask> createOneClick(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
|
||||
@Valid @RequestBody DeliveryRequests.OneClickOutbound request) {
|
||||
return ApiResponse.success(service.createOneClick(scope(user, customerId), operator(user), request));
|
||||
return ApiResponse.success(service.createOneClick(requiredCustomer(user, customerId), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/outbound-orders")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_LIST + "')")
|
||||
public ApiResponse<PageResult<DeliveryResponses.OutboundRow>> outboundPage(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@Valid DeliveryRequests.Query query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.outboundPage(listScope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/outbound-orders/{outboundNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_LIST + "')")
|
||||
public ApiResponse<DeliveryResponses.OutboundDetail> outboundDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String outboundNo, @Valid DeliveryRequests.PalletQuery query) {
|
||||
return ApiResponse.success(service.outboundDetail(scope(user, customerId), outboundNo, query));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.outboundDetail(outboundNo, query));
|
||||
}
|
||||
|
||||
@PostMapping("/outbound-orders/{outboundNo}/cancel")
|
||||
@Operation(summary = "Cancel outbound and linked delivery, restoring inventory")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CANCEL + "')")
|
||||
@Operation(summary = "Cancel an unexecuted outbound order and its linked delivery")
|
||||
public ApiResponse<Void> cancelOutbound(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String outboundNo,
|
||||
@Valid @RequestBody DeliveryRequests.Cancel request) {
|
||||
service.cancelOutbound(scope(user, customerId), operator(user), outboundNo, request);
|
||||
requirePlatform(user);
|
||||
service.cancelOutbound(outboundNo, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@@ -77,25 +84,27 @@ public class DeliveryTransferController {
|
||||
public ApiResponse<DeliveryResponses.DeliveryDetail> deliveryDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@PathVariable String deliveryNo) {
|
||||
return ApiResponse.success(service.deliveryDetail(scope(user, customerId), deliveryNo));
|
||||
return ApiResponse.success(service.deliveryDetail(requiredCustomer(user, customerId), deliveryNo));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
private String requiredCustomer(AuthenticatedUser user, String requestedCustomer) {
|
||||
requirePlatform(user);
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target customer");
|
||||
}
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requestedCustomer.trim();
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
private String listScope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) return requestedCustomer == null || requestedCustomer.isBlank() ? null : requestedCustomer.trim();
|
||||
return String.valueOf(user.tenantId());
|
||||
requirePlatform(user);
|
||||
return requestedCustomer == null || requestedCustomer.isBlank() ? null : requestedCustomer.trim();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -6,9 +6,11 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryResponses;
|
||||
import com.baiyue.lastleg.delivery.service.DeliveryTransferService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -19,19 +21,22 @@ public class MerchantDeliveryController {
|
||||
@Resource private DeliveryTransferService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_DELIVERY_LIST + "')")
|
||||
public ApiResponse<PageResult<DeliveryResponses.DeliveryRow>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid DeliveryRequests.Query query) {
|
||||
return ApiResponse.success(service.deliveryPage(customer(user), query));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_DELIVERY_CREATE + "')")
|
||||
public ApiResponse<DeliveryResponses.TransferResult> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String dispatchNo,
|
||||
@Valid @RequestBody DeliveryRequests.Transfer request) {
|
||||
return ApiResponse.success(service.transfer(customer(user), dispatchNo, operator(user), request));
|
||||
return ApiResponse.success(service.merchantTransfer(customer(user), dispatchNo, operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/{deliveryNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_DELIVERY_LIST + "')")
|
||||
public ApiResponse<DeliveryResponses.DeliveryDetail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo) {
|
||||
return ApiResponse.success(service.deliveryDetail(customer(user), deliveryNo));
|
||||
|
||||
+23
-14
@@ -5,37 +5,46 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryResponses;
|
||||
import com.baiyue.lastleg.delivery.service.DeliveryTransferService;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/outbound-tasks")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class OutboundPdaController {
|
||||
@Resource private DeliveryTransferService service;
|
||||
|
||||
@PostMapping("/{taskNo}/pick")
|
||||
public ApiResponse<DeliveryResponses.PdaTask> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String taskNo, @RequestParam(required=false) String customerId,
|
||||
@PathVariable String taskNo,
|
||||
@Valid @RequestBody DeliveryRequests.PdaPick request) {
|
||||
return ApiResponse.success(service.pdaPick(scope(user, customerId), operator(user), taskNo, request));
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pdaPick(String.valueOf(identity.overseasWarehouseId()),
|
||||
identity.operatorKey(), taskNo, request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<java.util.List<DeliveryResponses.PdaTask>> list(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required=false) String customerId, @RequestParam(required=false) String status) {
|
||||
return ApiResponse.success(service.pdaTasks(scope(user, customerId), status));
|
||||
@RequestParam(required=false) String status) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pdaTasks(String.valueOf(identity.overseasWarehouseId()), status));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskNo}")
|
||||
public ApiResponse<DeliveryResponses.PdaTaskDetail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String taskNo) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pdaTaskDetail(String.valueOf(identity.overseasWarehouseId()), taskNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{taskNo}/complete")
|
||||
public ApiResponse<DeliveryResponses.PdaTask> complete(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String taskNo, @RequestParam(required=false) String customerId) {
|
||||
return ApiResponse.success(service.completePda(scope(user, customerId), taskNo));
|
||||
@PathVariable String taskNo, @Valid @RequestBody DeliveryRequests.PdaComplete request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.completePda(String.valueOf(identity.overseasWarehouseId()), taskNo, request));
|
||||
}
|
||||
private String scope(AuthenticatedUser user, String requested) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (requested != null && !requested.isBlank()) return requested.trim();
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||
}
|
||||
|
||||
@@ -56,5 +56,6 @@ public final class DeliveryRequests {
|
||||
@NotBlank @Size(max=128) String sku) {}
|
||||
public record PdaPick(@NotNull Long outboundPalletItemId, @NotNull Long inventoryPositionId,
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record PdaComplete(@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record Cancel(@NotBlank @Size(max=1000) String reason) {}
|
||||
}
|
||||
|
||||
@@ -42,4 +42,11 @@ public final class DeliveryResponses {
|
||||
|
||||
public record OutboundDetail(OutboundRow outbound, DeliveryRow delivery) {}
|
||||
public record PdaTask(Long id, String taskNo, String status, String outboundNo, LocalDateTime pushedAt, LocalDateTime completedAt) {}
|
||||
public record PdaPickPosition(Long inventoryPositionId, String palletNo, String cartonMark,
|
||||
String skuCode, String inboundCode, long availablePackages,
|
||||
long pickedPackages) {}
|
||||
public record PdaTaskItem(Long outboundPalletItemId, String palletNo, String skuCode, String inboundCode,
|
||||
int packageCount, int pickedPackageCount, String status,
|
||||
List<PdaPickPosition> positions) {}
|
||||
public record PdaTaskDetail(PdaTask task, List<PdaTaskItem> items) {}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class OutboundPdaTask {
|
||||
private Long outboundOrderId;
|
||||
private String taskNo;
|
||||
private String status;
|
||||
private String completionIdempotencyKey;
|
||||
private LocalDateTime pushedAt;
|
||||
private LocalDateTime completedAt;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
+4
@@ -10,10 +10,14 @@ import org.apache.ibatis.annotations.Select;
|
||||
public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> {
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND outbound_no=#{outboundNo} LIMIT 1")
|
||||
OutboundOrder findByNo(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE outbound_no=#{outboundNo} LIMIT 1")
|
||||
OutboundOrder findGlobalByNo(@Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
OutboundOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND outbound_no=#{outboundNo} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockByNo(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE outbound_no=#{outboundNo} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockGlobalByNo(@Param("outboundNo") String outboundNo);
|
||||
}
|
||||
|
||||
+18
@@ -13,4 +13,22 @@ public interface OutboundPdaTaskMapper extends BaseMapper<OutboundPdaTask> {
|
||||
OutboundPdaTask lockByNo(@Param("customerId") String customerId, @Param("taskNo") String taskNo);
|
||||
@Select("SELECT * FROM ll_outbound_pda_task WHERE customer_id=#{customerId} AND (#{status} IS NULL OR status=#{status}) ORDER BY pushed_at DESC LIMIT 200")
|
||||
List<OutboundPdaTask> list(@Param("customerId") String customerId, @Param("status") String status);
|
||||
|
||||
@Select("SELECT t.* FROM ll_outbound_pda_task t JOIN ll_outbound_order o ON o.id=t.outbound_order_id " +
|
||||
"AND o.customer_id=t.customer_id WHERE t.task_no=#{taskNo} AND o.overseas_warehouse_id=#{warehouseId} " +
|
||||
"LIMIT 1 FOR UPDATE")
|
||||
OutboundPdaTask lockByNoAndWarehouse(@Param("taskNo") String taskNo, @Param("warehouseId") String warehouseId);
|
||||
|
||||
@Select("SELECT t.* FROM ll_outbound_pda_task t JOIN ll_outbound_order o ON o.id=t.outbound_order_id " +
|
||||
"AND o.customer_id=t.customer_id WHERE t.task_no=#{taskNo} AND o.overseas_warehouse_id=#{warehouseId} LIMIT 1")
|
||||
OutboundPdaTask findByNoAndWarehouse(@Param("taskNo") String taskNo, @Param("warehouseId") String warehouseId);
|
||||
|
||||
@Select("SELECT t.* FROM ll_outbound_pda_task t JOIN ll_outbound_order o ON o.id=t.outbound_order_id " +
|
||||
"AND o.customer_id=t.customer_id WHERE o.overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND (#{status} IS NULL OR t.status=#{status}) ORDER BY t.pushed_at DESC LIMIT 200")
|
||||
List<OutboundPdaTask> listByWarehouse(@Param("warehouseId") String warehouseId, @Param("status") String status);
|
||||
|
||||
@Select("SELECT * FROM ll_outbound_pda_task WHERE customer_id=#{customerId} " +
|
||||
"AND completion_idempotency_key=#{key} LIMIT 1")
|
||||
OutboundPdaTask findByCompletionKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
}
|
||||
|
||||
+172
-29
@@ -34,6 +34,8 @@ import com.baiyue.lastleg.delivery.mapper.OutboundPdaPickMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
@@ -69,6 +71,7 @@ public class DeliveryTransferService {
|
||||
@Resource private OutboundPdaTaskMapper pdaTasks;
|
||||
@Resource private OutboundPdaPickMapper pdaPicks;
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryCommandService inventoryCommands;
|
||||
@Resource private DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Resource private DropshipCartonMapper dropshipCartons;
|
||||
@@ -81,11 +84,26 @@ public class DeliveryTransferService {
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.TransferResult transfer(String customerId, String dispatchNo, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
public DeliveryResponses.TransferResult platformTransfer(String dispatchNo, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
DispatchOrder dispatch = dispatchOrders.lockGlobalByDispatchNo(trim(dispatchNo));
|
||||
if (dispatch == null) throw notFound("Dispatch order not found");
|
||||
return transferResolved(dispatch, operatorId, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.TransferResult merchantTransfer(String customerId, String dispatchNo, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
DispatchOrder found = dispatchOrders.findByDispatchNo(customerId, dispatchNo);
|
||||
if (found == null) throw notFound("Dispatch order not found");
|
||||
DispatchOrder dispatch = dispatchOrders.lockById(customerId, found.getId());
|
||||
if (dispatch == null) throw notFound("Dispatch order not found");
|
||||
return transferResolved(dispatch, operatorId, request);
|
||||
}
|
||||
|
||||
private DeliveryResponses.TransferResult transferResolved(DispatchOrder dispatch, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
String customerId = dispatch.getCustomerId();
|
||||
OutboundOrder replay = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
if (!dispatch.getId().equals(replay.getDispatchOrderId())) throw conflict("Idempotency key belongs to another dispatch");
|
||||
@@ -228,15 +246,42 @@ public class DeliveryTransferService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask pdaPick(String customerId, String operatorId, String taskNo, DeliveryRequests.PdaPick request) {
|
||||
OutboundPdaTask task = pdaTasks.lockByNo(customerId, taskNo); if (task == null) throw notFound("PDA task not found");
|
||||
OutboundPdaPick replay = pdaPicks.findByKey(customerId, request.idempotencyKey().trim()); if (replay != null) return taskResult(outboundOrders.selectById(task.getOutboundOrderId()), task);
|
||||
OutboundOrder outbound = outboundOrders.lockByNo(customerId, outboundNo(task.getOutboundOrderId()));
|
||||
public DeliveryResponses.PdaTask pdaPick(String warehouseId, String operatorId, String taskNo,
|
||||
DeliveryRequests.PdaPick request) {
|
||||
OutboundPdaTask task = pdaTasks.lockByNoAndWarehouse(required(taskNo, "PDA task number is required"),
|
||||
required(warehouseId, "PDA warehouse binding is required"));
|
||||
if (task == null) throw notFound("PDA task not found in the bound warehouse");
|
||||
String customerId = task.getCustomerId();
|
||||
OutboundPdaPick replay = pdaPicks.findByKey(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
if (!task.getId().equals(replay.getTaskId())
|
||||
|| !request.outboundPalletItemId().equals(replay.getOutboundPalletItemId())
|
||||
|| !request.inventoryPositionId().equals(replay.getInventoryPositionId())
|
||||
|| !request.packages().equals(replay.getPackages())) {
|
||||
throw conflict("Idempotency key was reused with a different outbound pick request");
|
||||
}
|
||||
return taskResult(requireOutbound(task), task);
|
||||
}
|
||||
OutboundOrder existingOutbound = requireOutbound(task);
|
||||
OutboundOrder outbound = outboundOrders.lockByNo(customerId, existingOutbound.getOutboundNo());
|
||||
if (outbound == null || !required(warehouseId, "PDA warehouse binding is required")
|
||||
.equals(outbound.getOverseasWarehouseId())) throw notFound("Outbound order not found in the bound warehouse");
|
||||
if (!Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus())) {
|
||||
throw conflict("PDA task is not available for picking");
|
||||
}
|
||||
OutboundPalletItem item = outboundPallets.selectById(request.outboundPalletItemId());
|
||||
if (item == null || !task.getOutboundOrderId().equals(item.getOutboundOrderId())) throw notFound("Outbound pallet item not found");
|
||||
if (item == null || !customerId.equals(item.getCustomerId())
|
||||
|| !task.getOutboundOrderId().equals(item.getOutboundOrderId())) throw notFound("Outbound pallet item not found");
|
||||
if (request.packages() > item.getPackageCount() - value(item.getPickedPackageCount())) throw conflict("Picked quantity exceeds outbound pallet quantity");
|
||||
InventoryPosition selectedPosition = inventoryPositions.selectById(request.inventoryPositionId());
|
||||
if (selectedPosition == null || !customerId.equals(selectedPosition.getCustomerId()) || !Objects.equals(selectedPosition.getPalletId(), item.getWarehousePalletId())) throw conflict("Scanned inventory position does not belong to the outbound pallet");
|
||||
if (!warehouseId.equals(selectedPosition.getOverseasWarehouseId())) {
|
||||
throw conflict("Scanned inventory position does not belong to the bound warehouse");
|
||||
}
|
||||
InventoryBalance selectedBalance = inventoryBalances.selectById(selectedPosition.getBalanceId());
|
||||
if (!matchesOutboundItem(customerId, warehouseId, item, selectedBalance)) {
|
||||
throw conflict("Scanned inventory position does not belong to the outbound item");
|
||||
}
|
||||
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, request.inventoryPositionId());
|
||||
if (profile == null) throw conflict("Scanned pallet is not one-click dropship inventory");
|
||||
List<DropshipCarton> cartons = dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
|
||||
@@ -258,37 +303,60 @@ public class DeliveryTransferService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask completePda(String customerId, String taskNo) {
|
||||
OutboundPdaTask task = pdaTasks.lockByNo(customerId, taskNo); if (task == null) throw notFound("PDA task not found");
|
||||
OutboundOrder outbound = outboundOrders.selectById(task.getOutboundOrderId());
|
||||
public DeliveryResponses.PdaTask completePda(String warehouseId, String taskNo,
|
||||
DeliveryRequests.PdaComplete request) {
|
||||
OutboundPdaTask task = pdaTasks.lockByNoAndWarehouse(required(taskNo, "PDA task number is required"),
|
||||
required(warehouseId, "PDA warehouse binding is required"));
|
||||
if (task == null) throw notFound("PDA task not found in the bound warehouse");
|
||||
String customerId = task.getCustomerId();
|
||||
OutboundOrder outbound = requireOutbound(task);
|
||||
String completionKey = request.idempotencyKey().trim();
|
||||
OutboundPdaTask completionReplay = pdaTasks.findByCompletionKey(customerId, completionKey);
|
||||
if (completionReplay != null && !task.getId().equals(completionReplay.getId())) {
|
||||
throw conflict("Idempotency key belongs to another outbound PDA task");
|
||||
}
|
||||
if ("COMPLETED".equals(task.getStatus())) {
|
||||
if (completionKey.equals(task.getCompletionIdempotencyKey())) {
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
throw conflict("PDA task was already completed with another idempotency key");
|
||||
}
|
||||
if (!Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus())) {
|
||||
throw conflict("PDA task is not available for completion");
|
||||
}
|
||||
List<OutboundPalletItem> items = outboundPallets.findByOutbound(customerId, outbound.getId());
|
||||
if (items.stream().anyMatch(i -> value(i.getPickedPackageCount()) < value(i.getPackageCount()))) throw conflict("All outbound pallets must be picked before completion");
|
||||
task.setStatus("COMPLETED"); task.setCompletedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task); outbound.setStatus("OUTBOUNDED"); outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(outbound); return taskResult(outbound, task);
|
||||
if (items.isEmpty() || items.stream().anyMatch(i -> value(i.getPickedPackageCount()) != value(i.getPackageCount()))) {
|
||||
throw conflict("All outbound pallets and packages must be picked before completion");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
task.setStatus("COMPLETED"); task.setCompletionIdempotencyKey(completionKey);
|
||||
task.setCompletedAt(now); task.setUpdatedAt(now); pdaTasks.updateById(task);
|
||||
outbound.setStatus("OUTBOUNDED"); outbound.setUpdatedAt(now); outboundOrders.updateById(outbound);
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelOutbound(String customerId, String operatorId, String outboundNo, DeliveryRequests.Cancel request) {
|
||||
OutboundOrder outbound = outboundOrders.lockByNo(customerId, outboundNo);
|
||||
public void cancelOutbound(String outboundNo, DeliveryRequests.Cancel request) {
|
||||
OutboundOrder outbound = outboundOrders.lockGlobalByNo(trim(outboundNo));
|
||||
if (outbound == null) throw notFound("Outbound order not found");
|
||||
if ("CANCELLED".equals(outbound.getStatus())) return;
|
||||
if (!Set.of("WAIT_OUTBOUND", "WAIT_PICK").contains(outbound.getStatus())) {
|
||||
throw conflict("Only an unexecuted outbound order can be cancelled");
|
||||
}
|
||||
String customerId = outbound.getCustomerId();
|
||||
List<OutboundPalletItem> outboundItems = outboundPallets.findByOutbound(customerId, outbound.getId());
|
||||
if (outboundItems.isEmpty() || outboundItems.stream().anyMatch(i -> !"PICKED".equals(i.getStatus())))
|
||||
throw conflict("Outbound order can only be cancelled after picking is completed");
|
||||
if (outboundItems.isEmpty() || outboundItems.stream().anyMatch(i -> value(i.getPickedPackageCount()) > 0
|
||||
|| !Set.of("WAIT_PICK", "WAIT_OUTBOUND").contains(i.getStatus()))) {
|
||||
throw conflict("Outbound order cannot be cancelled after execution has started");
|
||||
}
|
||||
DeliveryOrder existingDelivery = deliveryOrders.lockByOutbound(customerId, outbound.getId());
|
||||
if (existingDelivery != null && !"WAIT_DELIVERY".equals(existingDelivery.getStatus()))
|
||||
throw conflict("Outbound order cannot be cancelled while delivery is in progress");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (OutboundPdaPick pick : pdaPicks.findByOutbound(customerId, outbound.getId())) {
|
||||
inventoryCommands.adjust(customerId, operatorId,
|
||||
new InventoryCommandRequests.Adjustment(pick.getInventoryPositionId(), pick.getPackages(), "OUTBOUND_CANCEL",
|
||||
outbound.getOutboundNo(), outbound.getOutboundNo() + ":CANCEL:" + pick.getId(), request.reason()));
|
||||
OutboundPalletItem pickedItem = outboundItems.stream().filter(i -> Objects.equals(i.getId(), pick.getOutboundPalletItemId())).findFirst().orElse(null);
|
||||
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, pick.getInventoryPositionId());
|
||||
if (profile != null && pickedItem != null) dropshipSkuInventory.restore(customerId, profile.getId(), pickedItem.getSkuCode(), pick.getPackages());
|
||||
}
|
||||
dropshipCartons.restoreOutbound(customerId, outbound.getOutboundNo());
|
||||
DeliveryOrder delivery = existingDelivery;
|
||||
if (delivery != null) { delivery.setStatus("CANCELLED"); delivery.setUpdatedAt(now); deliveryOrders.updateById(delivery); }
|
||||
OutboundPdaTask task = pdaTasks.lockByOutbound(customerId, outbound.getId());
|
||||
if (task != null) { task.setStatus("CANCELLED"); task.setUpdatedAt(now); pdaTasks.updateById(task); }
|
||||
outbound.setStatus("CANCELLED"); outbound.setUpdatedAt(now); outboundOrders.updateById(outbound);
|
||||
for (OutboundPalletItem item : outboundItems) { item.setStatus("CANCELLED"); outboundPallets.updateById(item); }
|
||||
if (delivery != null) {
|
||||
@@ -299,10 +367,79 @@ public class DeliveryTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
public List<DeliveryResponses.PdaTask> pdaTasks(String customerId, String status) { return pdaTasks.list(customerId, StringUtils.hasText(status) ? status : null).stream().map(task -> taskResult(outboundOrders.selectById(task.getOutboundOrderId()), task)).toList(); }
|
||||
public List<DeliveryResponses.PdaTask> pdaTasks(String warehouseId, String status) {
|
||||
String boundWarehouseId = required(warehouseId, "PDA warehouse binding is required");
|
||||
return pdaTasks.listByWarehouse(boundWarehouseId, StringUtils.hasText(status) ? status.trim() : null).stream()
|
||||
.map(task -> taskResult(requireOutboundInWarehouse(task, boundWarehouseId), task)).toList();
|
||||
}
|
||||
|
||||
public DeliveryResponses.PdaTaskDetail pdaTaskDetail(String warehouseId, String taskNo) {
|
||||
String boundWarehouseId = required(warehouseId, "PDA warehouse binding is required");
|
||||
OutboundPdaTask task = pdaTasks.findByNoAndWarehouse(required(taskNo, "PDA task number is required"),
|
||||
boundWarehouseId);
|
||||
if (task == null) throw notFound("PDA task not found in the bound warehouse");
|
||||
OutboundOrder outbound = requireOutboundInWarehouse(task, boundWarehouseId);
|
||||
List<DeliveryResponses.PdaTaskItem> items = outboundPallets.findByOutbound(task.getCustomerId(),
|
||||
task.getOutboundOrderId()).stream().map(item -> new DeliveryResponses.PdaTaskItem(item.getId(),
|
||||
item.getPalletNo(), item.getSkuCode(), item.getInboundCode(), value(item.getPackageCount()),
|
||||
value(item.getPickedPackageCount()), item.getStatus(),
|
||||
pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
|
||||
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
|
||||
}
|
||||
|
||||
private List<DeliveryResponses.PdaPickPosition> pdaPickPositions(String customerId, String warehouseId,
|
||||
OutboundPalletItem item) {
|
||||
if (item.getWarehousePalletId() == null) return List.of();
|
||||
List<DeliveryResponses.PdaPickPosition> result = new ArrayList<>();
|
||||
for (InventoryPosition position : inventoryPositions.listPallet(customerId, item.getWarehousePalletId())) {
|
||||
long available = value(position.getAvailablePackages());
|
||||
if (available <= 0 || !warehouseId.equals(position.getOverseasWarehouseId())) continue;
|
||||
InventoryBalance balance = inventoryBalances.selectById(position.getBalanceId());
|
||||
if (!matchesOutboundItem(customerId, warehouseId, item, balance)) continue;
|
||||
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId());
|
||||
List<String> marks = profile == null ? List.of() : dropshipCartons.listProfile(customerId, profile.getId())
|
||||
.stream().filter(carton -> "AVAILABLE".equals(carton.getStatus()))
|
||||
.filter(carton -> Objects.equals(item.getSkuCode(), carton.getSkuCode()))
|
||||
.map(DropshipCarton::getCartonMark).filter(StringUtils::hasText).distinct().limit(available).toList();
|
||||
if (marks.isEmpty()) {
|
||||
result.add(pdaPosition(position, item, balance, null, available, value(position.getPickedPackages())));
|
||||
} else {
|
||||
for (String mark : marks) result.add(pdaPosition(position, item, balance, mark, 1, 0));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private DeliveryResponses.PdaPickPosition pdaPosition(InventoryPosition position, OutboundPalletItem item,
|
||||
InventoryBalance balance, String cartonMark,
|
||||
long available, long picked) {
|
||||
return new DeliveryResponses.PdaPickPosition(position.getId(), position.getPalletNo(), cartonMark,
|
||||
balance.getSkuCode(), balance.getInboundCode(), available, picked);
|
||||
}
|
||||
|
||||
private boolean matchesOutboundItem(String customerId, String warehouseId, OutboundPalletItem item,
|
||||
InventoryBalance balance) {
|
||||
return balance != null && customerId.equals(balance.getCustomerId())
|
||||
&& warehouseId.equals(balance.getOverseasWarehouseId())
|
||||
&& Objects.equals(item.getSkuCode(), balance.getSkuCode())
|
||||
&& Objects.equals(item.getInboundCode(), balance.getInboundCode());
|
||||
}
|
||||
|
||||
private DeliveryResponses.PdaTask taskResult(OutboundOrder outbound, OutboundPdaTask task) { return new DeliveryResponses.PdaTask(task.getId(), task.getTaskNo(), task.getStatus(), outbound.getOutboundNo(), task.getPushedAt(), task.getCompletedAt()); }
|
||||
private String outboundNo(Long id) { OutboundOrder value = outboundOrders.selectById(id); return value == null ? "" : value.getOutboundNo(); }
|
||||
private OutboundOrder requireOutbound(OutboundPdaTask task) {
|
||||
OutboundOrder outbound = outboundOrders.selectById(task.getOutboundOrderId());
|
||||
if (outbound == null || !task.getCustomerId().equals(outbound.getCustomerId())) {
|
||||
throw new IllegalStateException("Outbound PDA task has no scoped outbound order");
|
||||
}
|
||||
return outbound;
|
||||
}
|
||||
private OutboundOrder requireOutboundInWarehouse(OutboundPdaTask task, String warehouseId) {
|
||||
OutboundOrder outbound = requireOutbound(task);
|
||||
if (!warehouseId.equals(outbound.getOverseasWarehouseId())) {
|
||||
throw notFound("PDA task not found in the bound warehouse");
|
||||
}
|
||||
return outbound;
|
||||
}
|
||||
private int value(Integer value) { return value == null ? 0 : value; }
|
||||
private long value(Long value) { return value == null ? 0L : value; }
|
||||
|
||||
@@ -316,10 +453,11 @@ public class DeliveryTransferService {
|
||||
return page(page, page.getRecords().stream().map(this::outboundRow).toList());
|
||||
}
|
||||
|
||||
public DeliveryResponses.OutboundDetail outboundDetail(String customerId, String outboundNo,
|
||||
public DeliveryResponses.OutboundDetail outboundDetail(String outboundNo,
|
||||
DeliveryRequests.PalletQuery query) {
|
||||
OutboundOrder outbound = outboundOrders.findByNo(customerId, outboundNo);
|
||||
OutboundOrder outbound = outboundOrders.findGlobalByNo(trim(outboundNo));
|
||||
if (outbound == null) throw notFound("Outbound order not found");
|
||||
String customerId = outbound.getCustomerId();
|
||||
DeliveryOrder delivery = requireDelivery(customerId, outbound.getId());
|
||||
return new DeliveryResponses.OutboundDetail(outboundRow(outbound), deliveryRow(delivery));
|
||||
}
|
||||
@@ -404,6 +542,11 @@ public class DeliveryTransferService {
|
||||
private LastlegOrder firstTailOrder(String customerId, List<DispatchItem> items) { return items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).map(tailOrders::selectById).filter(Objects::nonNull).filter(order -> customerId.equals(order.getCustomerId())).findFirst().orElse(null); }
|
||||
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) { List<Long> ids = items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).distinct().toList(); if (ids.isEmpty()) return BigDecimal.ZERO; return tailOrderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>().eq(LastlegOrderDetail::getCustomerId, customerId).in(LastlegOrderDetail::getOrderId, ids)).stream().map(LastlegOrderDetail::getGrossWeightKg).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); }
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String required(String value, String message) {
|
||||
String normalized = trim(value);
|
||||
if (normalized == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
return normalized;
|
||||
}
|
||||
private static String firstText(String primary, String fallback) { return StringUtils.hasText(primary) ? primary.trim() : trim(fallback); }
|
||||
private static <T> PageResult<T> page(Page<?> page, List<T> records) { return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
|
||||
+12
-41
@@ -4,24 +4,20 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
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 com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@@ -30,60 +26,35 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
@Tag(name = "Tail-leg vehicle dispatch")
|
||||
public class DispatchController {
|
||||
@Resource private DispatchService service;
|
||||
@Resource private DispatchPdaWorkflowService pdaWorkflow;
|
||||
|
||||
@PutMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Edit vehicle, driver, appointment, route and departure data")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_EDIT + "')")
|
||||
@Operation(summary = "Edit vehicle, driver, appointment and route data")
|
||||
public ApiResponse<DispatchResponse> update(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo,
|
||||
@Valid @RequestBody DispatchRequests.Update request) {
|
||||
return ApiResponse.success(service.update(scope(user, customerId), dispatchNo, request));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.update(dispatchNo, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{dispatchNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_CANCEL + "')")
|
||||
@Operation(summary = "Cancel a pending dispatch and release its pallet locks")
|
||||
public ApiResponse<Void> cancel(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo) {
|
||||
service.cancel(scope(user, customerId), dispatchNo);
|
||||
requirePlatform(user);
|
||||
service.cancel(dispatchNo);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/{dispatchNo}/depart")
|
||||
@Operation(summary = "Ship all PDA-picked pallets and mark the dispatch departed")
|
||||
public ApiResponse<DispatchPdaResponses.Departed> depart(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@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));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform request must specify target tenant");
|
||||
}
|
||||
return requestedCustomer;
|
||||
}
|
||||
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");
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ public final class DispatchRequests {
|
||||
|
||||
public record Update(@Size(max = 64) String vehiclePlate, @Size(max = 128) String driverName,
|
||||
@Size(max = 64) String driverPhone, @Size(max = 255) String carrierName,
|
||||
@Size(max = 255) String route, LocalDateTime departAt,
|
||||
@Size(max = 255) String route,
|
||||
@Size(max = 64) String fbaAppointmentNo,
|
||||
@Size(max = 32) String fbaAppointmentStatus,
|
||||
@Size(max = 32) String vehicleAppointmentStatus,
|
||||
LocalDateTime estimatedArrivalAt, @Size(max = 32) String status) {
|
||||
LocalDateTime estimatedArrivalAt) {
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -6,15 +6,16 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> {
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1")
|
||||
DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} ORDER BY customer_id LIMIT 2")
|
||||
List<DispatchOrder> findAcrossCustomersByDispatchNo(@Param("dispatchNo") String dispatchNo);
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} LIMIT 1")
|
||||
DispatchOrder findGlobalByDispatchNo(@Param("dispatchNo") String dispatchNo);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} LIMIT 1 FOR UPDATE")
|
||||
DispatchOrder lockGlobalByDispatchNo(@Param("dispatchNo") String dispatchNo);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
||||
DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
|
||||
|
||||
+35
-40
@@ -5,65 +5,60 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
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.pda.security.PdaWarehouseIdentity;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/dispatches/{dispatchNo}")
|
||||
@RequestMapping("/api/pda/v1/last-leg/dispatches")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class DispatchPdaController {
|
||||
public static final String OPERATE_PERMISSION = "lastleg:pda:operate";
|
||||
@Resource private DispatchPdaWorkflowService service;
|
||||
|
||||
@PostMapping("/push")
|
||||
public ApiResponse<DispatchPdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@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
|
||||
public ApiResponse<DispatchPdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.task(scope(user, customerId), dispatchNo));
|
||||
public ApiResponse<List<DispatchPdaResponses.Task>> list(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String status) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.list(String.valueOf(identity.overseasWarehouseId()), status));
|
||||
}
|
||||
@PostMapping("/items/{itemId}/pick")
|
||||
|
||||
@GetMapping("/{dispatchNo}")
|
||||
public ApiResponse<DispatchPdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.task(String.valueOf(identity.overseasWarehouseId()), dispatchNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{dispatchNo}/items/{itemId}/pick")
|
||||
public ApiResponse<DispatchPdaResponses.Picked> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@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));
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pick(String.valueOf(identity.overseasWarehouseId()), dispatchNo, itemId,
|
||||
identity.operatorKey(), identity.operatorName(), request));
|
||||
}
|
||||
@PostMapping("/complete")
|
||||
|
||||
@PostMapping("/{dispatchNo}/complete")
|
||||
public ApiResponse<DispatchPdaResponses.PickingCompleted> completePicking(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@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));
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.completePicking(String.valueOf(identity.overseasWarehouseId()),
|
||||
dispatchNo, request));
|
||||
}
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
@PostMapping("/{dispatchNo}/depart")
|
||||
public ApiResponse<DispatchPdaResponses.Departed> depart(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@Valid @RequestBody DispatchPdaRequests.Depart request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.depart(String.valueOf(identity.overseasWarehouseId()), dispatchNo,
|
||||
identity.operatorKey(), request));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-13
@@ -8,19 +8,8 @@ import jakarta.validation.constraints.Size;
|
||||
|
||||
public final class DispatchPdaRequests {
|
||||
private DispatchPdaRequests() {}
|
||||
public record Push(@NotBlank String idempotencyKey, String operatorId, String operatorName,
|
||||
String warehouseId) {
|
||||
public Push(String idempotencyKey, String operatorId, String operatorName) {
|
||||
this(idempotencyKey, operatorId, operatorName, null);
|
||||
}
|
||||
}
|
||||
public record Pick(@NotNull Long positionId, @Positive long packages, @PositiveOrZero long productQuantity,
|
||||
@NotBlank String idempotencyKey, String remark, String warehouseId) {
|
||||
public Pick(Long positionId, long packages, long productQuantity, String idempotencyKey, String remark) {
|
||||
this(positionId, packages, productQuantity, idempotencyKey, remark, null);
|
||||
}
|
||||
}
|
||||
@NotBlank String idempotencyKey, String remark) {}
|
||||
public record Depart(@NotBlank String idempotencyKey, String remark) {}
|
||||
public record CompletePicking(@NotBlank String idempotencyKey,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
public record CompletePicking(@NotBlank String idempotencyKey) {}
|
||||
}
|
||||
|
||||
+5
@@ -8,4 +8,9 @@ import org.apache.ibatis.annotations.*;
|
||||
public interface DispatchPdaOperationMapper extends BaseMapper<DispatchPdaOperation> {
|
||||
@Select("SELECT * FROM ll_dispatch_pda_operation WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DispatchPdaOperation findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_pda_operation WHERE customer_id=#{customerId} AND task_id=#{taskId} " +
|
||||
"AND operation_type=#{operationType} AND status='COMPLETED' ORDER BY created_at DESC LIMIT 1")
|
||||
DispatchPdaOperation findCompleted(@Param("customerId") String customerId, @Param("taskId") Long taskId,
|
||||
@Param("operationType") String operationType);
|
||||
}
|
||||
|
||||
+7
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchPdaTaskMapper extends BaseMapper<DispatchPdaTask> {
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
@@ -11,4 +13,9 @@ public interface DispatchPdaTaskMapper extends BaseMapper<DispatchPdaTask> {
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1")
|
||||
DispatchPdaTask findByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE warehouse_id=#{warehouseId} " +
|
||||
"AND (#{status} IS NULL OR status=#{status}) ORDER BY pushed_at DESC LIMIT 200")
|
||||
List<DispatchPdaTask> listByWarehouse(@Param("warehouseId") String warehouseId,
|
||||
@Param("status") String status);
|
||||
}
|
||||
|
||||
+66
-55
@@ -44,59 +44,48 @@ public class DispatchPdaWorkflowService {
|
||||
@Resource private DispatchPickOrderAttributionService pickAttribution;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Task push(String customerId, String dispatchNo, DispatchPdaRequests.Push request) {
|
||||
DispatchOrder order = requireOrder(customerId, dispatchNo);
|
||||
requirePdaStatus(order);
|
||||
requireVehicle(order);
|
||||
String warehouseId = warehouse(request.warehouseId());
|
||||
requireDispatchWarehouse(order, warehouseId);
|
||||
DispatchPdaOperation replay = replay(customerId, "PUSH", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, DispatchPdaResponses.Task.class);
|
||||
items.bindReceivedPallets(customerId, order.getId());
|
||||
List<DispatchItem> dispatchItems = items.findByDispatchOrder(customerId, order.getId());
|
||||
if (dispatchItems.isEmpty()) throw conflict("Dispatch has no pallets to push");
|
||||
if (dispatchItems.stream().anyMatch(item -> item.getWarehousePalletId() == null)) {
|
||||
throw conflict("All pre-arranged pallets must be received before PDA push");
|
||||
@Transactional(readOnly = true)
|
||||
public List<DispatchPdaResponses.Task> list(String warehouseId, String status) {
|
||||
String boundWarehouseId = warehouse(warehouseId);
|
||||
List<DispatchPdaResponses.Task> result = new ArrayList<>();
|
||||
for (DispatchPdaTask pdaTask : tasks.listByWarehouse(boundWarehouseId, trim(status))) {
|
||||
DispatchOrder order = orders.selectById(pdaTask.getDispatchOrderId());
|
||||
if (order == null || !pdaTask.getCustomerId().equals(order.getCustomerId())
|
||||
|| !boundWarehouseId.equals(trim(order.getOverseasWarehouseId()))) {
|
||||
throw conflict("Dispatch PDA task scope is inconsistent");
|
||||
}
|
||||
result.add(task(order.getCustomerId(), order, pdaTask,
|
||||
items.findByDispatchOrder(order.getCustomerId(), order.getId())));
|
||||
}
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (task == null) {
|
||||
task = new DispatchPdaTask();
|
||||
task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(order.getId());
|
||||
task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING");
|
||||
task.setWarehouseId(warehouseId);
|
||||
task.setOperatorId(trim(request.operatorId())); task.setOperatorName(trim(request.operatorName()));
|
||||
task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); tasks.insert(task);
|
||||
}
|
||||
if ("PENDING_DEPARTURE".equals(order.getStatus())) { order.setStatus("PICKING"); order.setUpdatedAt(now); orders.updateById(order); }
|
||||
if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA task warehouse does not match selected warehouse");
|
||||
DispatchPdaResponses.Task result = task(customerId, order, task, dispatchItems);
|
||||
save(customerId, task.getId(), "PUSH", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DispatchPdaResponses.Task task(String customerId, String dispatchNo) {
|
||||
DispatchOrder order = requireOrder(customerId, dispatchNo);
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Task task(String warehouseId, String dispatchNo) {
|
||||
DispatchOrder order = requireWarehouseOrder(warehouseId, dispatchNo);
|
||||
String customerId = order.getCustomerId();
|
||||
items.bindReceivedPallets(customerId, order.getId());
|
||||
DispatchPdaTask task = tasks.findByDispatch(customerId, order.getId());
|
||||
if (task == null) throw notFound("Dispatch PDA task not found");
|
||||
requireTaskWarehouse(task, warehouseId);
|
||||
return task(customerId, order, task, items.findByDispatchOrder(customerId, order.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Picked pick(String customerId, String dispatchNo, long itemId,
|
||||
String operatorId, DispatchPdaRequests.Pick request) {
|
||||
DispatchOrder order = requireOrder(customerId, dispatchNo);
|
||||
public DispatchPdaResponses.Picked pick(String warehouseId, String dispatchNo, long itemId,
|
||||
String operatorId, String operatorName,
|
||||
DispatchPdaRequests.Pick request) {
|
||||
DispatchOrder order = requireWarehouseOrder(warehouseId, dispatchNo);
|
||||
String customerId = order.getCustomerId();
|
||||
requirePdaStatus(order);
|
||||
requireVehicle(order);
|
||||
String warehouseId = warehouse(request.warehouseId());
|
||||
requireDispatchWarehouse(order, warehouseId);
|
||||
items.bindReceivedPallets(customerId, order.getId());
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) {
|
||||
throw conflict("Dispatch PDA task is not available for picking");
|
||||
}
|
||||
if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA pick warehouse does not match task warehouse");
|
||||
bindOperator(task, operatorId, operatorName);
|
||||
DispatchItem item = items.selectById(itemId);
|
||||
if (item == null || !customerId.equals(item.getCustomerId()) || !order.getId().equals(item.getDispatchOrderId())) {
|
||||
throw notFound("Dispatch pallet item not found");
|
||||
@@ -145,9 +134,10 @@ public class DispatchPdaWorkflowService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchPdaResponses.PickingCompleted completePicking(String customerId, String dispatchNo,
|
||||
public DispatchPdaResponses.PickingCompleted completePicking(String warehouseId, String dispatchNo,
|
||||
DispatchPdaRequests.CompletePicking request) {
|
||||
DispatchOrder found = requireOrder(customerId, dispatchNo);
|
||||
DispatchOrder found = requireWarehouseOrder(warehouseId, dispatchNo);
|
||||
String customerId = found.getCustomerId();
|
||||
DispatchOrder order = orders.lockById(customerId, found.getId());
|
||||
if (order == null) throw notFound("Dispatch order not found");
|
||||
DispatchPdaOperation replay = replay(customerId, "COMPLETE_PICKING", request.idempotencyKey(), request);
|
||||
@@ -159,8 +149,12 @@ public class DispatchPdaWorkflowService {
|
||||
long planned = completedItems.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull)
|
||||
.mapToLong(Integer::longValue).sum();
|
||||
long picked = picks.listTask(customerId, task.getId()).stream().mapToLong(p -> n(p.getPackages())).sum();
|
||||
return new DispatchPdaResponses.PickingCompleted(order.getDispatchNo(), order.getStatus(), planned,
|
||||
picked, Math.max(0, planned - picked), picked < planned, true);
|
||||
DispatchPdaResponses.PickingCompleted result = new DispatchPdaResponses.PickingCompleted(
|
||||
order.getDispatchNo(), order.getStatus(), planned, picked, Math.max(0, planned - picked), false, false);
|
||||
DispatchPdaOperation completed = operations.findCompleted(customerId, task.getId(), "COMPLETE_PICKING");
|
||||
if (completed != null) throw conflict("PDA picking was already completed with another idempotency key");
|
||||
save(customerId, task.getId(), "COMPLETE_PICKING", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) {
|
||||
throw conflict("Dispatch PDA task is not available for completion");
|
||||
@@ -169,25 +163,27 @@ public class DispatchPdaWorkflowService {
|
||||
long planned = source.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull)
|
||||
.mapToLong(Integer::longValue).sum();
|
||||
long picked = picks.listTask(customerId, task.getId()).stream().mapToLong(p -> n(p.getPackages())).sum();
|
||||
if (picked <= 0 || picked > planned) throw conflict("PDA completion requires a valid picked package count");
|
||||
boolean partial = picked < planned;
|
||||
if (planned <= 0 || picked != planned || !allPicked(customerId, task.getId(), source)) {
|
||||
throw conflict("Every dispatch pallet and package must be fully picked before completion");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
task.setStatus("COMPLETED"); task.setCompletedAt(now); task.setUpdatedAt(now); tasks.updateById(task);
|
||||
order.setStatus(partial ? "PARTIAL_READY_TO_DEPART" : "READY_TO_DEPART");
|
||||
order.setPartialCompletionReason(partial ? request.reason().trim() : null);
|
||||
order.setStatus("READY_TO_DEPART");
|
||||
order.setPartialCompletionReason(null);
|
||||
order.setPickedPackageCount((int) picked);
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
DispatchPdaResponses.PickingCompleted result = new DispatchPdaResponses.PickingCompleted(order.getDispatchNo(),
|
||||
order.getStatus(), planned, picked, planned - picked, partial, false);
|
||||
order.getStatus(), planned, picked, 0, false, false);
|
||||
save(customerId, task.getId(), "COMPLETE_PICKING", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Final warehouse exit: only fully PDA-picked dispatches can consume picked inventory. */
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Departed depart(String customerId, String dispatchNo, String operatorId,
|
||||
public DispatchPdaResponses.Departed depart(String warehouseId, String dispatchNo, String operatorId,
|
||||
DispatchPdaRequests.Depart request) {
|
||||
DispatchOrder found = requireOrder(customerId, dispatchNo);
|
||||
DispatchOrder found = requireWarehouseOrder(warehouseId, dispatchNo);
|
||||
String customerId = found.getCustomerId();
|
||||
DispatchOrder order = orders.lockById(customerId, found.getId());
|
||||
if (order == null) throw notFound("Dispatch order not found");
|
||||
DispatchPdaOperation replay = replay(customerId, "DEPART", request.idempotencyKey(), request);
|
||||
@@ -199,13 +195,13 @@ public class DispatchPdaWorkflowService {
|
||||
}
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
if (task == null || !"COMPLETED".equals(task.getStatus())
|
||||
|| !("READY_TO_DEPART".equals(order.getStatus()) || "PARTIAL_READY_TO_DEPART".equals(order.getStatus()))) {
|
||||
|| !"READY_TO_DEPART".equals(order.getStatus())) {
|
||||
throw conflict("Dispatch must complete PDA picking before departure");
|
||||
}
|
||||
requireTaskWarehouse(task, warehouseId);
|
||||
|
||||
List<DispatchItem> dispatchItems = items.findByDispatchOrder(customerId, order.getId());
|
||||
boolean partial = "PARTIAL_READY_TO_DEPART".equals(order.getStatus());
|
||||
if (!partial && !allPicked(customerId, task.getId(), dispatchItems)) {
|
||||
if (!allPicked(customerId, task.getId(), dispatchItems)) {
|
||||
throw conflict("Every dispatch pallet must be fully picked before departure");
|
||||
}
|
||||
List<DispatchPdaPick> dispatchPicks = picks.listTask(customerId, task.getId());
|
||||
@@ -217,7 +213,7 @@ public class DispatchPdaWorkflowService {
|
||||
outbound.setUpdatedAt(now);
|
||||
outboundOrders.updateById(outbound);
|
||||
order.setShippedPackageCount((int) shipped);
|
||||
order.setStatus(partial ? "PARTIAL_DEPARTED" : "DEPARTED");
|
||||
order.setStatus("DEPARTED");
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
releasePalletLocks(customerId, order.getId(), now);
|
||||
@@ -258,18 +254,33 @@ public class DispatchPdaWorkflowService {
|
||||
palletLocks.updateById(lock);
|
||||
}
|
||||
}
|
||||
private DispatchOrder requireOrder(String customerId, String dispatchNo) { DispatchOrder order = orders.findByDispatchNo(customerId, dispatchNo); if (order == null) throw notFound("Dispatch order not found"); return order; }
|
||||
private DispatchOrder requireWarehouseOrder(String warehouseId, String dispatchNo) {
|
||||
String boundWarehouseId = warehouse(warehouseId);
|
||||
DispatchOrder order = orders.findGlobalByDispatchNo(required(dispatchNo, "Dispatch number is required"));
|
||||
if (order == null || !boundWarehouseId.equals(trim(order.getOverseasWarehouseId()))) {
|
||||
throw notFound("Dispatch order not found in the bound warehouse");
|
||||
}
|
||||
return order;
|
||||
}
|
||||
private void requirePdaStatus(DispatchOrder order) {
|
||||
if (!("PENDING_DEPARTURE".equals(order.getStatus()) || "PICKING".equals(order.getStatus())
|
||||
|| "READY_TO_DEPART".equals(order.getStatus()))) throw conflict("Dispatch has already departed and cannot be changed by PDA");
|
||||
}
|
||||
private void requireVehicle(DispatchOrder order) { if (trim(order.getVehiclePlate()) == null) throw conflict("Vehicle plate is required before PDA push or picking"); }
|
||||
private static String warehouse(String value) { return required(value, "Warehouse selection is required for PDA work"); }
|
||||
private static void requireDispatchWarehouse(DispatchOrder order, String warehouseId) {
|
||||
if (trim(order.getOverseasWarehouseId()) == null || !warehouseId.equals(order.getOverseasWarehouseId().trim())) {
|
||||
throw conflict("Selected warehouse does not match dispatch warehouse");
|
||||
private static void requireTaskWarehouse(DispatchPdaTask task, String warehouseId) {
|
||||
if (!warehouse(warehouseId).equals(trim(task.getWarehouseId()))) {
|
||||
throw notFound("Dispatch PDA task not found in the bound warehouse");
|
||||
}
|
||||
}
|
||||
private static void bindOperator(DispatchPdaTask task, String operatorId, String operatorName) {
|
||||
String normalizedOperator = required(operatorId, "PDA operator is required");
|
||||
if (trim(task.getOperatorId()) != null && !normalizedOperator.equals(trim(task.getOperatorId()))) {
|
||||
throw conflict("Dispatch PDA task is already assigned to another operator");
|
||||
}
|
||||
task.setOperatorId(normalizedOperator);
|
||||
task.setOperatorName(trim(operatorName));
|
||||
}
|
||||
private DispatchPdaOperation replay(String customerId, String type, String key, Object request) { DispatchPdaOperation op = operations.findKey(customerId, required(key, "Idempotency key is required")); if (op == null) return null; if (!type.equals(op.getOperationType()) || !hash(request).equals(op.getRequestHash())) throw conflict("Idempotency key was reused with a different request"); if (!"COMPLETED".equals(op.getStatus())) throw conflict("PDA operation is still processing"); return op; }
|
||||
private void save(String customerId, Long taskId, String type, String key, Object request, Object result) { DispatchPdaOperation op = new DispatchPdaOperation(); op.setId(IdWorker.getId()); op.setCustomerId(customerId); op.setTaskId(taskId); op.setOperationType(type); op.setIdempotencyKey(required(key,"Idempotency key is required")); op.setRequestHash(hash(request)); op.setResultJson(json(result)); op.setStatus("COMPLETED"); op.setCreatedAt(LocalDateTime.now()); op.setUpdatedAt(op.getCreatedAt()); operations.insert(op); }
|
||||
private <T> T read(DispatchPdaOperation op, Class<T> type) { try { return objectMapper.readValue(op.getResultJson(), type); } catch (Exception e) { throw new IllegalStateException("Cannot read PDA replay", e); } }
|
||||
|
||||
+28
-18
@@ -20,6 +20,8 @@ import com.baiyue.lastleg.dispatch.mapper.DispatchDraftContainerMapper;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchContainerResponse;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchDraftResponse;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -49,6 +51,7 @@ public class DispatchService {
|
||||
@Resource private DispatchDraftMapper drafts;
|
||||
@Resource private DispatchDraftContainerMapper draftContainers;
|
||||
@Resource private DispatchCandidateMapper candidates;
|
||||
@Resource private DispatchPdaTaskMapper pdaTasks;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
|
||||
public String resolveCustomerScopeForPlans(List<Long> planIds) {
|
||||
@@ -315,6 +318,17 @@ public class DispatchService {
|
||||
item.setCreatedAt(now);
|
||||
items.insert(item);
|
||||
}
|
||||
DispatchPdaTask pdaTask = new DispatchPdaTask();
|
||||
pdaTask.setId(IdWorker.getId());
|
||||
pdaTask.setCustomerId(customerId);
|
||||
pdaTask.setDispatchOrderId(dispatch.getId());
|
||||
pdaTask.setTaskNo("DPDA" + pdaTask.getId());
|
||||
pdaTask.setStatus("PDA_PENDING");
|
||||
pdaTask.setWarehouseId(dispatch.getOverseasWarehouseId());
|
||||
pdaTask.setPushedAt(now);
|
||||
pdaTask.setCreatedAt(now);
|
||||
pdaTask.setUpdatedAt(now);
|
||||
pdaTasks.insert(pdaTask);
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
@@ -351,49 +365,45 @@ public class DispatchService {
|
||||
}
|
||||
|
||||
public DispatchResponse platformDetail(String dispatchNo) {
|
||||
List<DispatchOrder> matches = orders.findAcrossCustomersByDispatchNo(dispatchNo);
|
||||
if (matches == null || matches.isEmpty()) {
|
||||
DispatchOrder dispatch = orders.findGlobalByDispatchNo(trimToNull(dispatchNo));
|
||||
if (dispatch == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
}
|
||||
if (matches.size() > 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Dispatch number is not unique across customers");
|
||||
}
|
||||
DispatchOrder dispatch = matches.get(0);
|
||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchResponse update(String customerId, String dispatchNo, DispatchRequests.Update request) {
|
||||
DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
|
||||
public DispatchResponse update(String dispatchNo, DispatchRequests.Update request) {
|
||||
DispatchOrder dispatch = orders.lockGlobalByDispatchNo(trimToNull(dispatchNo));
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
// Vehicle plate is intentionally editable through all dispatch states.
|
||||
if (!PENDING_DEPARTURE.equals(dispatch.getStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Only a pending dispatch can be edited");
|
||||
}
|
||||
String customerId = dispatch.getCustomerId();
|
||||
dispatch.setVehiclePlate(trimToNull(request.vehiclePlate()));
|
||||
dispatch.setDriverName(trimToNull(request.driverName()));
|
||||
dispatch.setDriverPhone(trimToNull(request.driverPhone()));
|
||||
dispatch.setCarrierName(trimToNull(request.carrierName()));
|
||||
dispatch.setRoute(trimToNull(request.route()));
|
||||
dispatch.setDepartAt(request.departAt());
|
||||
dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo()));
|
||||
dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus()));
|
||||
dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus()));
|
||||
dispatch.setEstimatedArrivalAt(request.estimatedArrivalAt());
|
||||
if (StringUtils.hasText(request.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);
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancel(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
|
||||
public void cancel(String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.lockGlobalByDispatchNo(trimToNull(dispatchNo));
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
if (CANCELLED.equals(dispatch.getStatus())) return;
|
||||
if (!(PENDING_DEPARTURE.equals(dispatch.getStatus()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Dispatched order cannot be cancelled");
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Only a pending dispatch can be cancelled");
|
||||
}
|
||||
String customerId = dispatch.getCustomerId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
dispatch.setStatus(CANCELLED);
|
||||
dispatch.setUpdatedAt(now);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.baiyue.lastleg.security;
|
||||
|
||||
/** Canonical Admin authorities for fourth-phase overseas APIs. */
|
||||
/** Canonical authorities for overseas browser APIs. */
|
||||
public final class OverseasPermissions {
|
||||
public static final String LAST_MILE_ORDER_LIST = "platform:overseas-warehouse:last-mile-order:list";
|
||||
public static final String LAST_MILE_ORDER_EDIT = "platform:overseas-warehouse:last-mile-order:edit";
|
||||
@@ -20,6 +20,15 @@ public final class OverseasPermissions {
|
||||
public static final String INVENTORY_EDIT = "platform:overseas-warehouse:inventory:edit";
|
||||
public static final String INVENTORY_ADJUST = "platform:overseas-warehouse:inventory:adjust";
|
||||
public static final String DELIVERY_LIST = "platform:overseas-warehouse:delivery:list";
|
||||
public static final String DISPATCH_EDIT = "platform:overseas-warehouse:dispatch:edit";
|
||||
public static final String DISPATCH_CANCEL = "platform:overseas-warehouse:dispatch:cancel";
|
||||
public static final String DISPATCH_TRANSFER_DELIVERY =
|
||||
"platform:overseas-warehouse:dispatch:transfer-delivery";
|
||||
public static final String OUTBOUND_LIST = "platform:overseas-warehouse:outbound:list";
|
||||
public static final String OUTBOUND_CREATE = "platform:overseas-warehouse:outbound:create";
|
||||
public static final String OUTBOUND_CANCEL = "platform:overseas-warehouse:outbound:cancel";
|
||||
public static final String MERCHANT_DELIVERY_LIST = "merchant:warehouse:delivery:list";
|
||||
public static final String MERCHANT_DELIVERY_CREATE = "merchant:warehouse:delivery:create";
|
||||
public static final String PURCHASE_LIST = "platform:overseas-business:purchase:list";
|
||||
public static final String PURCHASE_EDIT = "platform:overseas-business:purchase:edit";
|
||||
public static final String PURCHASE_REVIEW = "platform:overseas-business:purchase:review";
|
||||
|
||||
Reference in New Issue
Block a user