优化
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ public class DeliveryTransferController {
|
||||
@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(requiredCustomer(user, customerId), operator(user), request));
|
||||
}
|
||||
|
||||
@@ -80,7 +80,12 @@ public final class DeliveryRequests {
|
||||
}
|
||||
}
|
||||
public record PdaPick(@NotNull Long outboundPalletItemId, Long inventoryPositionId,
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks) {
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of());
|
||||
}
|
||||
}
|
||||
public record PdaComplete(@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record Cancel(@NotBlank @Size(max=1000) String reason) {}
|
||||
}
|
||||
|
||||
+73
-1
@@ -50,6 +50,12 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -88,6 +94,9 @@ public class DeliveryTransferService {
|
||||
@Resource private LastlegOrderMapper tailOrders;
|
||||
@Resource private LastlegOrderStatusHistoryMapper histories;
|
||||
@Resource private LastlegOrderDetailMapper tailOrderDetails;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelApplicationService relabelService;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
|
||||
@Transactional
|
||||
@@ -358,7 +367,7 @@ public class DeliveryTransferService {
|
||||
if (replay != null) {
|
||||
if (!task.getId().equals(replay.getTaskId())
|
||||
|| !request.outboundPalletItemId().equals(replay.getOutboundPalletItemId())
|
||||
|| !request.inventoryPositionId().equals(replay.getInventoryPositionId())
|
||||
|| !Objects.equals(request.inventoryPositionId(), replay.getInventoryPositionId())
|
||||
|| !request.packages().equals(replay.getPackages())) {
|
||||
throw conflict("Idempotency key was reused with a different outbound pick request");
|
||||
}
|
||||
@@ -393,6 +402,62 @@ public class DeliveryTransferService {
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
if ("RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
if (request.inventoryPositionId() == null) throw conflict("换标出库必须确认实际库存位置");
|
||||
InventoryPosition selectedPosition = inventoryPositions.selectById(request.inventoryPositionId());
|
||||
if (selectedPosition == null || !customerId.equals(selectedPosition.getCustomerId())
|
||||
|| !warehouseId.equals(selectedPosition.getOverseasWarehouseId())
|
||||
|| !Objects.equals(selectedPosition.getPalletId(), item.getWarehousePalletId())) {
|
||||
throw conflict("确认的库存位置不属于该换标出库托盘");
|
||||
}
|
||||
List<String> expectedMarks = Arrays.stream(Optional.ofNullable(item.getCartonMarks()).orElse("").split(","))
|
||||
.map(String::trim).filter(StringUtils::hasText).distinct().sorted().toList();
|
||||
List<String> submittedMarks = request.cartonMarks() == null ? List.of() : request.cartonMarks();
|
||||
List<String> scannedMarks = submittedMarks.stream().map(String::trim).filter(StringUtils::hasText)
|
||||
.distinct().sorted().toList();
|
||||
if (scannedMarks.size() != submittedMarks.size() || !expectedMarks.equals(scannedMarks)) {
|
||||
throw conflict("必须扫描并提交该换标出库任务的全部指定箱唛");
|
||||
}
|
||||
if (request.packages().intValue() != value(item.getPackageCount()) || value(item.getPickedPackageCount()) > 0) {
|
||||
throw conflict("换标出库托盘必须按指定箱唛一次完成拆托确认");
|
||||
}
|
||||
RelabelApplication application = relabelApplications.lockByOutbound(customerId, outbound.getOutboundNo());
|
||||
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
|
||||
throw conflict("换标单尚未完成客户确认或已不可出库");
|
||||
}
|
||||
List<RelabelItem> sourceItems = relabelItems.selectList(new LambdaQueryWrapper<RelabelItem>()
|
||||
.eq(RelabelItem::getApplicationId, application.getId())
|
||||
.eq(RelabelItem::getCustomerId, customerId)
|
||||
.eq(RelabelItem::getSourceInventoryPositionId, request.inventoryPositionId()));
|
||||
long sourcePackages = sourceItems.stream().mapToLong(source -> value(source.getNotifiedPackageCount())).sum();
|
||||
if (sourcePackages != request.packages()) throw conflict("换标出库来源库存快照与任务箱数不一致");
|
||||
int releaseIndex = 0;
|
||||
for (RelabelItem source : sourceItems) {
|
||||
inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
request.inventoryPositionId(), value(source.getNotifiedPackageCount()), application.getRelabelNo(), null,
|
||||
"RELABEL:" + application.getRelabelNo(), request.idempotencyKey() + ":RELEASE:" + releaseIndex++,
|
||||
"换标出库拆托前释放原冻结库存"));
|
||||
}
|
||||
inventoryCommands.allocate(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(),
|
||||
request.packages(), application.getRelabelNo(), outbound.getOutboundNo(), null,
|
||||
request.idempotencyKey() + ":ALLOCATE", "换标出库指定箱唛拆托"));
|
||||
inventoryCommands.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(),
|
||||
request.packages(), application.getRelabelNo(), outbound.getOutboundNo(), null,
|
||||
request.idempotencyKey() + ":PICK", "换标出库指定箱唛拣货"));
|
||||
inventoryCommands.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(),
|
||||
request.packages(), application.getRelabelNo(), outbound.getOutboundNo(), null,
|
||||
request.idempotencyKey() + ":SHIP", "换标出库指定箱唛出库"));
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId);
|
||||
pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(request.inventoryPositionId());
|
||||
pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
|
||||
List<OutboundPalletItem> pickedItems = outboundPallets.findByOutbound(customerId, outbound.getId());
|
||||
if (!pickedItems.isEmpty() && pickedItems.stream().allMatch(value -> "PICKED".equals(value.getStatus()))) {
|
||||
outbound.setStatus("PICKED"); outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(outbound);
|
||||
}
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
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())) {
|
||||
@@ -452,6 +517,13 @@ public class DeliveryTransferService {
|
||||
task.setStatus("COMPLETED"); task.setCompletionIdempotencyKey(completionKey);
|
||||
task.setCompletedAt(now); task.setUpdatedAt(now); pdaTasks.updateById(task);
|
||||
outbound.setStatus("OUTBOUNDED"); outbound.setUpdatedAt(now); outboundOrders.updateById(outbound);
|
||||
if ("RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
RelabelApplication application = relabelApplications.lockByOutbound(customerId, outbound.getOutboundNo());
|
||||
if (application != null && RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
|
||||
relabelService.changeStatus(customerId, application.getRelabelNo(), "PDA_OUTBOUND",
|
||||
new RelabelRequests.ChangeStatus("COMPLETED", "换标出库PDA作业完成"));
|
||||
}
|
||||
}
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,15 @@ public final class DispatchRequests {
|
||||
@Size(max = 255) String route, @Size(max = 64) String fbaAppointmentNo,
|
||||
@Size(max = 32) String fbaAppointmentStatus,
|
||||
@Size(max = 32) String vehicleAppointmentStatus,
|
||||
@Size(max = 64) String draftToken) {
|
||||
@Size(max = 64) String draftToken,
|
||||
@Size(max = 64) String relabelNo) {
|
||||
public PreArrange(List<Long> planPalletIds, String route, String fbaAppointmentNo,
|
||||
String fbaAppointmentStatus, String vehicleAppointmentStatus) {
|
||||
this(planPalletIds, route, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, null);
|
||||
this(planPalletIds, route, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, null, null);
|
||||
}
|
||||
public PreArrange(List<Long> planPalletIds, String route, String fbaAppointmentNo,
|
||||
String fbaAppointmentStatus, String vehicleAppointmentStatus, String draftToken) {
|
||||
this(planPalletIds, route, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, draftToken, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,5 +34,8 @@ public class DispatchItem {
|
||||
private BigDecimal volumeCbm;
|
||||
/** PLAN for planned pallets; RECEIVED for warehouse/entity pallets. */
|
||||
private String sourceStage;
|
||||
private String cartonMarks;
|
||||
private String sourceBusinessType;
|
||||
private String sourceBusinessNo;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
+28
@@ -143,4 +143,32 @@ public interface DispatchCandidateMapper {
|
||||
"</script>")
|
||||
List<DispatchPalletCandidate> lockSelected(@Param("customerId") String customerId,
|
||||
@Param("planPalletIds") List<Long> planPalletIds);
|
||||
|
||||
@Select("<script>" +
|
||||
"SELECT NULL AS planId,NULL AS planPalletId,pos.pallet_id AS warehousePalletId,a.lastleg_order_id AS orderId," +
|
||||
"a.customer_id AS customerId,NULL AS customerName,NULL AS planNo,'RELABEL_APPLICATION' AS orderType,NULL AS volumeWeightKg,NULL AS containerNo," +
|
||||
"NULL AS appointmentAt,NULL AS expectedLoadingAt,a.overseas_warehouse_id AS overseasWarehouseId,MIN(ri.source_warehouse_name) AS overseasWarehouseName," +
|
||||
"MIN(a.relabel_no) AS orderNo,a.relabel_no AS orderRefs,MIN(ri.source_inbound_code) AS inboundCode,pos.pallet_no AS palletNo,NULL AS destinationWarehouseId," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseName," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
|
||||
"SUM(ri.notified_package_count) AS packageCount,NULL AS volumeCbm,'RELABEL' AS sourceStage," +
|
||||
"GROUP_CONCAT(DISTINCT CONCAT(ri.carton_mark,'->',ri.fba_carton_mark) ORDER BY ri.carton_mark SEPARATOR ',') AS cartonMarks " +
|
||||
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
|
||||
"JOIN ll_inventory_position pos ON pos.id=ri.source_inventory_position_id AND pos.customer_id=a.customer_id " +
|
||||
"WHERE a.delivery_method='TRUCK' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND a.customer_id=#{customerId} </if>" +
|
||||
"<if test='relabelNo != null and relabelNo != \"\"'>AND a.relabel_no=#{relabelNo} </if>" +
|
||||
"<if test='orderNo != null and orderNo != \"\"'>AND a.relabel_no LIKE CONCAT('%',#{orderNo},'%') </if>" +
|
||||
"AND pos.available_packages > 0 " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=a.customer_id AND di.source_stage='RELABEL' AND di.order_refs=a.relabel_no) " +
|
||||
"GROUP BY a.id,a.lastleg_order_id,a.customer_id,a.overseas_warehouse_id,a.relabel_no,pos.pallet_id,pos.pallet_no" +
|
||||
"</script>")
|
||||
List<DispatchPalletCandidate> relabelAvailable(@Param("customerId") String customerId,
|
||||
@Param("orderNo") String orderNo,
|
||||
@Param("relabelNo") String relabelNo);
|
||||
|
||||
@org.apache.ibatis.annotations.Update("UPDATE ll_relabel_application SET fulfillment_status='PRE_DISPATCH_CREATED',updated_at=CURRENT_TIMESTAMP(3) " +
|
||||
"WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND fulfillment_status='WAIT_PRE_DISPATCH'")
|
||||
int markRelabelPreDispatched(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
}
|
||||
|
||||
+29
-1
@@ -84,8 +84,15 @@ public class DispatchService {
|
||||
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode,
|
||||
String sourceStage, String draftToken) {
|
||||
validateSourceStage(sourceStage);
|
||||
return candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||
List<DispatchPalletCandidate> result = candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||
trimToNull(sourceStage), trimToNull(draftToken));
|
||||
if (!StringUtils.hasText(sourceStage) || "RECEIVED".equals(sourceStage)) {
|
||||
List<DispatchPalletCandidate> relabel = candidates.relabelAvailable(null, trimToNull(orderNo), null);
|
||||
if (relabel != null && !relabel.isEmpty()) {
|
||||
result = java.util.stream.Stream.concat(result.stream(), relabel.stream()).toList();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode, String sourceStage) {
|
||||
return platformCandidates(orderNo, fbaWarehouseCode, sourceStage, null);
|
||||
@@ -212,6 +219,9 @@ public class DispatchService {
|
||||
|
||||
@Transactional
|
||||
public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
||||
if (StringUtils.hasText(request.relabelNo())) {
|
||||
return preArrangeRelabel(customerId, operatorId, request);
|
||||
}
|
||||
if (!StringUtils.hasText(customerId)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"No customer business data is available for dispatch");
|
||||
@@ -355,6 +365,24 @@ public class DispatchService {
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
private DispatchResponse preArrangeRelabel(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
||||
List<DispatchPalletCandidate> selected = candidates.relabelAvailable(customerId, null, request.relabelNo());
|
||||
Set<Long> requested = request.planPalletIds().stream().map(Long::valueOf).collect(Collectors.toSet());
|
||||
selected = selected.stream().filter(c -> requested.contains(c.warehousePalletId())).toList();
|
||||
if (selected.isEmpty() || selected.size() != requested.size()) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单托盘候选已变化,请刷新后重试");
|
||||
String warehouse = selected.get(0).overseasWarehouseId();
|
||||
if (selected.stream().anyMatch(c -> !Objects.equals(warehouse, c.overseasWarehouseId()))) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单只能选择同一海外仓");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DispatchOrder dispatch = new DispatchOrder(); dispatch.setId(IdWorker.getId()); dispatch.setCustomerId(customerId);
|
||||
dispatch.setDispatchNo(orderNoGenerator.next("DISPATCH", warehouse, dispatch.getId())); dispatch.setOverseasWarehouseId(warehouse); dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
|
||||
dispatch.setRoute(StringUtils.hasText(request.route()) ? request.route().trim() : route(selected)); dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo())); dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus())); dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus())); dispatch.setStatus(PENDING_DEPARTURE); dispatch.setItemCount(selected.size()); dispatch.setPalletCount(selected.size()); dispatch.setPackageCount(selected.stream().map(DispatchPalletCandidate::packageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); dispatch.setPickedPackageCount(0); dispatch.setShippedPackageCount(0); dispatch.setCreatedBy(trimToNull(operatorId)); dispatch.setCreatedAt(now); dispatch.setUpdatedAt(now); orders.insert(dispatch);
|
||||
for (DispatchPalletCandidate candidate : selected) { DispatchItem item = new DispatchItem(); item.setId(IdWorker.getId()); item.setDispatchOrderId(dispatch.getId()); item.setCustomerId(customerId); item.setWarehousePalletId(candidate.warehousePalletId()); item.setOrderId(candidate.orderId()); item.setOverseasWarehouseId(candidate.overseasWarehouseId()); item.setOverseasWarehouseName(candidate.overseasWarehouseName()); item.setOrderNo(candidate.orderNo()); item.setOrderRefs(candidate.orderRefs()); item.setInboundCode(candidate.inboundCode()); item.setPalletNo(candidate.palletNo()); item.setDestinationWarehouseCode(candidate.destinationWarehouseCode()); item.setDestinationWarehouseName(candidate.destinationWarehouseName()); item.setFbaWarehouseCode(candidate.fbaWarehouseCode()); item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0)); item.setVolumeCbm(BigDecimal.ZERO); item.setSourceStage("RELABEL"); item.setCartonMarks(candidate.cartonMarks()); item.setSourceBusinessType("RELABEL_ORDER"); item.setSourceBusinessNo(request.relabelNo()); item.setCreatedAt(now); items.insert(item); }
|
||||
DispatchPdaTask task = new DispatchPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(dispatch.getId()); task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING"); task.setWarehouseId(warehouse); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task);
|
||||
if (candidates.markRelabelPreDispatched(customerId, request.relabelNo()) != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单已被其他排车占用");
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
private void assertNoDetainedOrders(String customerId, List<DispatchPalletCandidate> selected) {
|
||||
List<Long> orderIds = selected.stream().map(DispatchPalletCandidate::orderId)
|
||||
.filter(Objects::nonNull).distinct().sorted().toList();
|
||||
|
||||
+7
@@ -42,6 +42,13 @@ public class PdaPalletOperationController {
|
||||
return ApiResponse.success(service.disassemble(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/relabel-disassemble")
|
||||
public ApiResponse<PalletOperationResponses.Operation> relabelDisassemble(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.RelabelDisassemble request) {
|
||||
return ApiResponse.success(service.relabelDisassemble(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/swap")
|
||||
public ApiResponse<PalletOperationResponses.Operation> swap(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+8
@@ -31,6 +31,14 @@ public final class PalletOperationRequests {
|
||||
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
/** 换标场景按箱唛拆托并绑定到一个目标托盘。箱唛位置由服务端从换标单快照解析。 */
|
||||
public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@NotBlank @Size(max = 64) String relabelNo,
|
||||
@Valid PalletExpectation source,
|
||||
@Valid PalletExpectation target,
|
||||
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> cartonMarks,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
public record Swap(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation source,
|
||||
@Valid PalletExpectation target,
|
||||
|
||||
+117
-14
@@ -13,6 +13,11 @@ import com.baiyue.lastleg.palletops.entity.PdaPalletOperation;
|
||||
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
|
||||
import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
|
||||
import com.baiyue.lastleg.palletops.model.PalletInventoryRow;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelPalletMutationService;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -52,6 +57,9 @@ public class PdaPalletOperationService {
|
||||
@Resource private PalletPhysicalGateway physical;
|
||||
@Resource private PdaWarehouseService history;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelPalletMutationService relabelPalletMutations;
|
||||
private TransactionTemplate transaction;
|
||||
private TransactionTemplate newTransaction;
|
||||
|
||||
@@ -113,6 +121,64 @@ public class PdaPalletOperationService {
|
||||
false));
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation relabelDisassemble(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.RelabelDisassemble request) {
|
||||
PalletOperationResponses.Operation replay = replayIfPresent(identity, "RELABEL_PALLET_DISASSEMBLE",
|
||||
request.idempotencyKey(), request);
|
||||
if (replay != null) return replay;
|
||||
String relabelNo = required(request.relabelNo(), "换标单号不能为空");
|
||||
RelabelApplication application = relabelApplications.findAnyByRelabelNo(relabelNo);
|
||||
if (application == null || !"READY_FOR_OUTBOUND".equals(application.getStatus())) {
|
||||
throw conflict("换标单尚未完成客户确认,不能执行拆托");
|
||||
}
|
||||
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("换标单不属于 Token 冻结的海外仓");
|
||||
}
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
request.source(), List.of(request.target()));
|
||||
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode()));
|
||||
PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode()));
|
||||
if (source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同");
|
||||
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(value -> !value.isEmpty()).toList();
|
||||
if (marks.size() != request.cartonMarks().size() || new HashSet<>(marks).size() != marks.size()) {
|
||||
throw badRequest("箱唛不能为空且不能重复");
|
||||
}
|
||||
List<RelabelItem> selected = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", application.getId()).eq("customer_id", application.getCustomerId())
|
||||
.in("carton_mark", marks));
|
||||
if (selected.size() != marks.size()
|
||||
|| selected.stream().map(RelabelItem::getCartonMark).distinct().count() != marks.size()) {
|
||||
throw conflict("提交的箱唛不属于当前换标单");
|
||||
}
|
||||
Map<Long, Long> packagesByPosition = new LinkedHashMap<>();
|
||||
Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>();
|
||||
for (RelabelItem item : selected) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
|
||||
|| item.getNotifiedPackageCount() <= 0) throw conflict("换标箱唛缺少库存位置或箱数快照");
|
||||
InventoryPosition position = positions.selectById(item.getSourceInventoryPositionId());
|
||||
if (position == null || !Objects.equals(position.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(position.getPalletId(), source.palletId())
|
||||
|| !Objects.equals(position.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("扫描的原托盘与换标箱唛实际库存位置不一致");
|
||||
}
|
||||
packagesByPosition.merge(position.getId(), item.getNotifiedPackageCount().longValue(), Long::sum);
|
||||
itemsByPosition.computeIfAbsent(position.getId(), ignored -> new ArrayList<>()).add(item);
|
||||
}
|
||||
List<PlannedMove> moves = packagesByPosition.entrySet().stream().map(entry -> {
|
||||
InventoryPosition position = positions.selectById(entry.getKey());
|
||||
if (value(position.getHoldPackages()) < entry.getValue()) throw conflict("换标冻结库存箱数不足");
|
||||
return new PlannedMove(entry.getKey(), entry.getValue(), value(position.getVersion()), source.palletId(),
|
||||
source.palletCode(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()), InventoryBucket.HOLD);
|
||||
}).toList();
|
||||
requireCommonDestination(identity.overseasWarehouseId(), moves, List.of(target));
|
||||
Plan base = plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false);
|
||||
Plan relabelPlan = new Plan(base.pallets(), base.moves(), base.changes(),
|
||||
new RelabelMutation(application, itemsByPosition));
|
||||
return execute(identity, "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(), request,
|
||||
request.reason(), relabelPlan);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Swap request) {
|
||||
PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_SWAP",
|
||||
@@ -130,7 +196,7 @@ public class PdaPalletOperationService {
|
||||
requireMovable(row, value(row.getAvailablePackages()));
|
||||
return new PlannedMove(row.getPositionId(), value(row.getAvailablePackages()), value(row.getVersion()),
|
||||
source.palletId(), source.palletCode(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()));
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()), InventoryBucket.AVAILABLE);
|
||||
}).toList();
|
||||
return execute(identity, "PALLET_SWAP", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false));
|
||||
@@ -149,7 +215,7 @@ public class PdaPalletOperationService {
|
||||
requireMovable(row, value(row.getAvailablePackages()));
|
||||
return new PlannedMove(row.getPositionId(), value(row.getAvailablePackages()), value(row.getVersion()),
|
||||
source.palletId(), source.palletCode(), 0L, "UNBOUND", source.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), source.locationId()));
|
||||
locationCode(identity.overseasWarehouseId(), source.locationId()), InventoryBucket.AVAILABLE);
|
||||
}).toList();
|
||||
return execute(identity, "PALLET_UNBIND", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(), true));
|
||||
@@ -183,8 +249,10 @@ public class PdaPalletOperationService {
|
||||
|
||||
private PalletOperationResponses.Operation mutate(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
Plan plan, String key, String requestHash, String reason) {
|
||||
if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId());
|
||||
Set<Long> seen = new HashSet<>();
|
||||
LinkedHashSet<String> customers = new LinkedHashSet<>();
|
||||
Map<Long, InventoryPosition> relabelTargets = new LinkedHashMap<>();
|
||||
for (PlannedMove move : plan.moves()) {
|
||||
if (!seen.add(move.positionId())) throw conflict("同一库存行不能在一次托盘作业中重复移动");
|
||||
PalletInventoryRow locked = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()),
|
||||
@@ -193,10 +261,14 @@ public class PdaPalletOperationService {
|
||||
throw conflict("库存所在托盘已变化,请重新扫码");
|
||||
}
|
||||
if (value(locked.getVersion()) != move.expectedVersion()) throw conflict("库存版本已变化,请重新扫码");
|
||||
requireMovable(locked, move.packages());
|
||||
if (move.bucket() == InventoryBucket.HOLD) requireHeld(locked, move.packages());
|
||||
else requireMovable(locked, move.packages());
|
||||
customers.add(locked.getCustomerId());
|
||||
moveInventory(identity, operation, locked, move, key, reason);
|
||||
InventoryPosition target = moveInventory(identity, operation, locked, move, key, reason);
|
||||
if (plan.relabelMutation() != null) relabelTargets.put(move.positionId(), target);
|
||||
}
|
||||
if (plan.relabelMutation() != null) relabelPalletMutations.moveSelectedItems(
|
||||
plan.relabelMutation().application(), plan.relabelMutation().itemsByPosition(), relabelTargets);
|
||||
for (PalletChange change : plan.changes()) {
|
||||
long actual = mapper.palletPackages(String.valueOf(identity.overseasWarehouseId()), change.pallet().palletId());
|
||||
boolean shouldBeEmpty = "IDLE".equals(change.targetStatus());
|
||||
@@ -219,12 +291,22 @@ public class PdaPalletOperationService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void moveInventory(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
PalletInventoryRow sourceRow, PlannedMove move, String key, String reason) {
|
||||
private void lockRelabelMutation(RelabelMutation mutation, long warehouseId) {
|
||||
RelabelApplication locked = relabelApplications.lockAnyByRelabelNo(mutation.application().getRelabelNo());
|
||||
if (locked == null || !"READY_FOR_OUTBOUND".equals(locked.getStatus())
|
||||
|| !Objects.equals(locked.getCustomerId(), mutation.application().getCustomerId())
|
||||
|| !Objects.equals(locked.getOverseasWarehouseId(), String.valueOf(warehouseId))
|
||||
|| !Objects.equals(locked.getOutboundOrderNo(), mutation.application().getOutboundOrderNo())) {
|
||||
throw conflict("换标单状态或履约单据已变化,请重新扫码");
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryPosition moveInventory(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
PalletInventoryRow sourceRow, PlannedMove move, String key, String reason) {
|
||||
InventoryPosition source = positions.selectById(sourceRow.getPositionId());
|
||||
if (source == null) throw conflict("库存行不存在");
|
||||
long sourceBefore = value(source.getAvailablePackages());
|
||||
source.setAvailablePackages(sourceBefore - move.packages());
|
||||
long sourceBefore = bucketValue(source, move.bucket());
|
||||
setBucketValue(source, move.bucket(), sourceBefore - move.packages());
|
||||
source.setStatus(positionPackages(source) == 0 ? "EMPTY" : "OCCUPIED");
|
||||
source.setVersion(value(source.getVersion()) + 1); source.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(source);
|
||||
@@ -248,8 +330,8 @@ public class PdaPalletOperationService {
|
||||
if (target == null) throw duplicate;
|
||||
}
|
||||
}
|
||||
long targetBefore = value(target.getAvailablePackages());
|
||||
target.setAvailablePackages(targetBefore + move.packages()); target.setStatus("OCCUPIED");
|
||||
long targetBefore = bucketValue(target, move.bucket());
|
||||
setBucketValue(target, move.bucket(), targetBefore + move.packages()); target.setStatus("OCCUPIED");
|
||||
target.setWarehouseLocationId(move.targetLocationId());
|
||||
target.setWarehouseLocationCode(move.targetLocationCode());
|
||||
target.setVersion(value(target.getVersion()) + 1); target.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -261,6 +343,7 @@ public class PdaPalletOperationService {
|
||||
sourceBefore, -move.packages(), sourceBefore - move.packages(), move.sourcePalletCode(), move.targetPalletCode(), reason);
|
||||
ledger(identity, operation, balance, target, key + ":" + source.getId() + ":IN",
|
||||
targetBefore, move.packages(), targetBefore + move.packages(), move.sourcePalletCode(), move.targetPalletCode(), reason);
|
||||
return target;
|
||||
}
|
||||
|
||||
private void ledger(PdaWarehouseIdentity identity, PdaPalletOperation operation, InventoryBalance balance,
|
||||
@@ -302,7 +385,7 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
changes.sort(Comparator.comparingInt((PalletChange change) -> "IDLE".equals(change.targetStatus()) ? 0 : 1)
|
||||
.thenComparingLong(change -> change.pallet().palletId()));
|
||||
return new Plan(pallets, moves, changes);
|
||||
return new Plan(pallets, moves, changes, null);
|
||||
}
|
||||
|
||||
private List<PlannedMove> plannedLines(long warehouseId, List<PalletOperationRequests.MoveLine> lines,
|
||||
@@ -325,7 +408,7 @@ public class PdaPalletOperationService {
|
||||
requireMovable(row, line.packages());
|
||||
result.add(new PlannedMove(row.getPositionId(), line.packages(), line.expectedPositionVersion(),
|
||||
row.getPalletId(), row.getPalletNo(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(warehouseId, target.locationId())));
|
||||
locationCode(warehouseId, target.locationId()), InventoryBucket.AVAILABLE));
|
||||
}
|
||||
requireUniqueLines(result);
|
||||
return result;
|
||||
@@ -494,6 +577,21 @@ public class PdaPalletOperationService {
|
||||
throw conflict("冻结、已分配、已拣或收货中的库存不能执行托盘作业");
|
||||
}
|
||||
}
|
||||
private static void requireHeld(PalletInventoryRow row, long packages) {
|
||||
if (packages <= 0 || value(row.getHoldPackages()) < packages) throw conflict("换标冻结库存箱数不足");
|
||||
if (value(row.getAllocatedPackages()) > 0 || value(row.getPickedPackages()) > 0
|
||||
|| value(row.getReceivingPackages()) > 0) {
|
||||
throw conflict("已分配、已拣或收货中的库存不能执行换标拆托");
|
||||
}
|
||||
}
|
||||
private static long bucketValue(InventoryPosition position, InventoryBucket bucket) {
|
||||
return bucket == InventoryBucket.HOLD ? value(position.getHoldPackages())
|
||||
: value(position.getAvailablePackages());
|
||||
}
|
||||
private static void setBucketValue(InventoryPosition position, InventoryBucket bucket, long packages) {
|
||||
if (bucket == InventoryBucket.HOLD) position.setHoldPackages(packages);
|
||||
else position.setAvailablePackages(packages);
|
||||
}
|
||||
private static void requireUniqueLines(List<PlannedMove> moves) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (moves.stream().anyMatch(move -> !ids.add(move.positionId()))) throw badRequest("库存行不能重复提交");
|
||||
@@ -517,6 +615,7 @@ public class PdaPalletOperationService {
|
||||
private static String first(String first, String second) { String value = trim(first); return value == null ? trim(second) : value; }
|
||||
private static String limit(String value, int size) { if (value == null) return null; return value.length() <= size ? value : value.substring(0, size); }
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static int value(Integer value) { return value == null ? 0 : value; }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
@@ -524,9 +623,13 @@ public class PdaPalletOperationService {
|
||||
private record PlannedMove(long positionId, long packages, long expectedVersion,
|
||||
long sourcePalletId, String sourcePalletCode,
|
||||
long targetPalletId, String targetPalletCode, long targetLocationId,
|
||||
String targetLocationCode) { }
|
||||
String targetLocationCode, InventoryBucket bucket) { }
|
||||
private record PalletChange(PalletPhysicalGateway.PalletReference pallet, String targetStatus,
|
||||
long targetLocationId) { }
|
||||
private record Plan(Map<String, PalletPhysicalGateway.PalletReference> pallets,
|
||||
List<PlannedMove> moves, List<PalletChange> changes) { }
|
||||
List<PlannedMove> moves, List<PalletChange> changes,
|
||||
RelabelMutation relabelMutation) { }
|
||||
private record RelabelMutation(RelabelApplication application,
|
||||
Map<Long, List<RelabelItem>> itemsByPosition) { }
|
||||
private enum InventoryBucket { AVAILABLE, HOLD }
|
||||
}
|
||||
|
||||
+33
@@ -12,6 +12,11 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@@ -34,10 +39,38 @@ public class MerchantRelabelApplicationController {
|
||||
@GetMapping("/{relabelNo}")
|
||||
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo) { return ApiResponse.success(service.detail(customer(user), relabelNo)); }
|
||||
@PostMapping(value = "/{relabelNo}/shipping-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ApiResponse<RelabelResponses.Detail> uploadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @RequestPart("file") MultipartFile file) {
|
||||
return ApiResponse.success(service.uploadShippingLabel(customer(user), relabelNo, operatorLong(user), file));
|
||||
}
|
||||
@GetMapping("/{relabelNo}/shipping-label/{fileId}/download")
|
||||
public ResponseEntity<byte[]> downloadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @PathVariable long fileId) {
|
||||
var file = service.downloadShippingLabel(customer(user), relabelNo, fileId);
|
||||
return download(file.content(), file.fileName(), file.contentType());
|
||||
}
|
||||
@GetMapping("/{relabelNo}/completion-photos/{fileId}/download")
|
||||
public ResponseEntity<byte[]> downloadCompletionPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @PathVariable long fileId) {
|
||||
var file = service.downloadCompletionPhoto(customer(user), relabelNo, fileId);
|
||||
return download(file.content(), file.fileName(), file.contentType());
|
||||
}
|
||||
@PostMapping("/{relabelNo}/confirm-completion")
|
||||
public ApiResponse<RelabelResponses.Detail> confirmCompletion(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @Valid @RequestBody RelabelRequests.CustomerConfirm request) {
|
||||
return ApiResponse.success(service.confirmCompletion(customer(user), relabelNo, operator(user), request));
|
||||
}
|
||||
private ResponseEntity<byte[]> download(byte[] content, String name, String type) {
|
||||
return ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore())
|
||||
.header("Content-Disposition", ContentDisposition.inline().filename(name, StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(MediaType.parseMediaType(type)).contentLength(content.length).body(content);
|
||||
}
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||
private long operatorLong(AuthenticatedUser user) { return user.userId() == null ? 0L : user.userId(); }
|
||||
}
|
||||
|
||||
+10
@@ -85,6 +85,16 @@ public class RelabelApplicationController {
|
||||
return ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore()).header("Content-Disposition", ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(org.springframework.http.MediaType.parseMediaType(file.contentType())).contentLength(file.content().length).body(file.content());
|
||||
}
|
||||
@GetMapping("/{relabelNo}/completion-photos/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
public ResponseEntity<byte[]> downloadCompletionPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @PathVariable long fileId) {
|
||||
requirePlatform(user);
|
||||
var file = service.downloadCompletionPhoto(service.customerId(relabelNo), relabelNo, fileId);
|
||||
return ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore())
|
||||
.header("Content-Disposition", ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(MediaType.parseMediaType(file.contentType())).contentLength(file.content().length).body(file.content());
|
||||
}
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
|
||||
@@ -26,8 +26,17 @@ public final class RelabelRequests {
|
||||
@NotNull @DecimalMin(value = "0.01") BigDecimal chargeAmount,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items
|
||||
) {}
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items,
|
||||
@Size(max = 32) String deliveryMethod,
|
||||
Boolean printShippingLabel
|
||||
) {
|
||||
public Create(String customerOrderNo, String operationWarehouseId, LocalDateTime labelDeadline,
|
||||
String contactName, String contactPhone, BigDecimal chargeAmount, String remark,
|
||||
String idempotencyKey, List<Item> items) {
|
||||
this(customerOrderNo, operationWarehouseId, labelDeadline, contactName, contactPhone,
|
||||
chargeAmount, remark, idempotencyKey, items, "TRUCK", false);
|
||||
}
|
||||
}
|
||||
|
||||
public record Item(
|
||||
@NotNull Long sourceInventoryBalanceId,
|
||||
@@ -105,4 +114,7 @@ public final class RelabelRequests {
|
||||
|
||||
public record ChangeStatus(@NotBlank @Size(max = 32) String status,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ public final class RelabelResponses {
|
||||
int totalPackageCount, int notifiedPackageCount, String skuCodes,
|
||||
String totalFileId, String totalFileName, String totalFileUrl,
|
||||
BigDecimal chargeAmount, String chargeCurrency, String walletStatus,
|
||||
String walletErrorMessage,
|
||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
|
||||
LocalDateTime createdAt) {}
|
||||
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
@@ -37,6 +39,8 @@ public final class RelabelResponses {
|
||||
String status) {}
|
||||
public record History(String fromStatus, String toStatus, String operatorId,
|
||||
String remark, LocalDateTime createdAt) {}
|
||||
public record CompletionEvidence(Long taskId, List<Long> photoFileIds, String completionRemark,
|
||||
String reviewerName, String reviewRemark, LocalDateTime completedAt) {}
|
||||
public record Detail(Summary application, String remark, List<Item> items, List<History> history,
|
||||
List<TailOrder> tailOrders) {}
|
||||
List<TailOrder> tailOrders, CompletionEvidence completionEvidence) {}
|
||||
}
|
||||
|
||||
+9
@@ -20,6 +20,15 @@ public class RelabelApplication {
|
||||
private LocalDateTime labelDeadline;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private String deliveryMethod;
|
||||
private Boolean printShippingLabel;
|
||||
private String shippingLabelFileId;
|
||||
private String shippingLabelFileName;
|
||||
private String shippingLabelFileUrl;
|
||||
private String fulfillmentStatus;
|
||||
private String outboundOrderNo;
|
||||
private String customerConfirmationKey;
|
||||
private LocalDateTime customerConfirmedAt;
|
||||
private String remark;
|
||||
private String totalFileId;
|
||||
private String totalFileName;
|
||||
|
||||
+3
@@ -22,4 +22,7 @@ public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication>
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
RelabelApplication lockAnyByRelabelNo(@Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND outbound_order_no=#{outboundNo} LIMIT 1 FOR UPDATE")
|
||||
RelabelApplication lockByOutbound(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
|
||||
}
|
||||
|
||||
+19
@@ -10,6 +10,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -89,6 +90,24 @@ public class HttpRelabelPhotoFileGateway implements RelabelPhotoFileGateway {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadedFile download(String customerId, long taskId, long fileId) {
|
||||
validate(customerId, taskId, fileId);
|
||||
ResponseEntity<byte[]> response = client.get()
|
||||
.uri("/internal/file/v1/business-files/{tenantId}/{businessType}/{businessId}/{fileId}/content",
|
||||
tenantId(customerId), BUSINESS_TYPE, taskId, fileId)
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new ResponseStatusException(result.getStatusCode().is4xxClientError()
|
||||
? HttpStatus.NOT_FOUND : HttpStatus.SERVICE_UNAVAILABLE, "Relabel photo download failed");
|
||||
}).toEntity(byte[].class);
|
||||
String fileName = response.getHeaders().getContentDisposition().getFilename();
|
||||
String contentType = response.getHeaders().getContentType() == null
|
||||
? MediaType.APPLICATION_OCTET_STREAM_VALUE : response.getHeaders().getContentType().toString();
|
||||
return new DownloadedFile(response.getBody() == null ? new byte[0] : response.getBody(),
|
||||
fileName == null ? "relabel-photo" : fileName, contentType);
|
||||
}
|
||||
|
||||
private static long tenantId(String customerId) {
|
||||
try { return Long.parseLong(customerId); }
|
||||
catch (NumberFormatException exception) {
|
||||
|
||||
+2
@@ -5,6 +5,8 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
public interface RelabelPhotoFileGateway {
|
||||
UploadedFile upload(String customerId, long taskId, long operatorId, MultipartFile file);
|
||||
void validate(String customerId, long taskId, long fileId);
|
||||
DownloadedFile download(String customerId, long taskId, long fileId);
|
||||
|
||||
record UploadedFile(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
record DownloadedFile(byte[] content, String fileName, String contentType) { }
|
||||
}
|
||||
|
||||
+2
-2
@@ -252,8 +252,8 @@ public class PdaRelabelService {
|
||||
updateTask(task, request.expectedVersion());
|
||||
if ("APPROVE".equals(decision)) {
|
||||
applicationService.changeStatus(application.getCustomerId(), relabelNo, operatorKey,
|
||||
new RelabelRequests.ChangeStatus(COMPLETED, request.remark()));
|
||||
application.setStatus(COMPLETED);
|
||||
new RelabelRequests.ChangeStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, request.remark()));
|
||||
application.setStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING);
|
||||
}
|
||||
insertHistory(task, "REVIEW_" + decision, before, after, operatorKey, operatorName,
|
||||
Map.of("decision", decision, "remark", Objects.toString(trim(request.remark()), "")));
|
||||
|
||||
+205
-8
@@ -24,6 +24,15 @@ import com.baiyue.lastleg.relabel.entity.RelabelStatusHistory;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask;
|
||||
import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPdaTask;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundPdaTaskMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.wallet.WalletCommandOutboxService;
|
||||
import com.baiyue.lastleg.wallet.WalletResultHandler;
|
||||
@@ -52,7 +61,11 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class RelabelApplicationService implements WalletResultHandler {
|
||||
public static final String PENDING_PROCESSING = "PENDING_PROCESSING";
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_PROCESSING, "PROCESSING", "COMPLETED", "REJECTED", "CANCELLED");
|
||||
public static final String CUSTOMER_CONFIRM_PENDING = "CUSTOMER_CONFIRM_PENDING";
|
||||
public static final String READY_FOR_OUTBOUND = "READY_FOR_OUTBOUND";
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_PROCESSING, "PROCESSING", CUSTOMER_CONFIRM_PENDING,
|
||||
READY_FOR_OUTBOUND, "COMPLETED", "REJECTED", "CANCELLED");
|
||||
private static final Set<String> DELIVERY_METHODS = Set.of("TRUCK", "EXPRESS", "TRUCK_PICKUP", "EXPRESS_PICKUP", "PICKUP");
|
||||
|
||||
@Resource private RelabelApplicationMapper applications;
|
||||
@Resource private RelabelItemMapper items;
|
||||
@@ -66,6 +79,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private RelabelDocumentFileGateway documentFiles;
|
||||
@Resource private PdaRelabelTaskMapper pdaTasks;
|
||||
@Resource private RelabelPhotoFileGateway photoFiles;
|
||||
@Resource private OutboundOrderMapper outboundOrders;
|
||||
@Resource private OutboundPalletItemMapper outboundItems;
|
||||
@Resource private OutboundPdaTaskMapper outboundTasks;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String customerId(String relabelNo) { return require(null, relabelNo).getCustomerId(); }
|
||||
@@ -94,6 +112,48 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(application.getCustomerId(), relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail uploadShippingLabel(String customerId, String relabelNo, long operatorId,
|
||||
MultipartFile file) {
|
||||
RelabelApplication application = lockForDocument(customerId, relabelNo);
|
||||
if (!Boolean.TRUE.equals(application.getPrintShippingLabel())) {
|
||||
throw conflict("This relabel order does not require a shipping label");
|
||||
}
|
||||
requireShippingLabel(file);
|
||||
var uploaded = documentFiles.upload(application.getCustomerId(), "OVERSEAS_RELABEL_SHIPPING_LABEL",
|
||||
String.valueOf(application.getId()), operatorId, file);
|
||||
application.setShippingLabelFileId(String.valueOf(uploaded.fileId()));
|
||||
application.setShippingLabelFileName(uploaded.fileName());
|
||||
application.setShippingLabelFileUrl("/api/merchant/v1/last-leg/relabel-applications/"
|
||||
+ java.net.URLEncoder.encode(relabelNo, StandardCharsets.UTF_8) + "/shipping-label/"
|
||||
+ uploaded.fileId() + "/download");
|
||||
application.setUpdatedAt(LocalDateTime.now());
|
||||
applications.updateById(application);
|
||||
return detail(application.getCustomerId(), relabelNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RelabelDocumentFileGateway.DownloadedFile downloadShippingLabel(String customerId, String relabelNo,
|
||||
long fileId) {
|
||||
RelabelApplication application = require(customerId, relabelNo);
|
||||
if (!String.valueOf(fileId).equals(application.getShippingLabelFileId())) {
|
||||
throw notFound("Shipping label does not belong to this relabel order");
|
||||
}
|
||||
return documentFiles.download(application.getCustomerId(), "OVERSEAS_RELABEL_SHIPPING_LABEL",
|
||||
String.valueOf(application.getId()), fileId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RelabelPhotoFileGateway.DownloadedFile downloadCompletionPhoto(String customerId, String relabelNo,
|
||||
long fileId) {
|
||||
RelabelApplication application = require(customerId, relabelNo);
|
||||
PdaRelabelTask task = pdaTasks.find(application.getCustomerId(), application.getId());
|
||||
if (task == null || !readIds(task.getCompletionPhotoFileIds()).contains(fileId)) {
|
||||
throw notFound("Completion photo does not belong to this relabel order");
|
||||
}
|
||||
return photoFiles.download(application.getCustomerId(), task.getId(), fileId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RelabelDocumentFileGateway.DownloadedFile downloadFile(String customerId, String relabelNo, String scope, Long itemId, long fileId) {
|
||||
RelabelApplication application = require(customerId, relabelNo);
|
||||
@@ -131,6 +191,15 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
} catch (java.io.IOException exception) { throw badRequest("无法读取PDF文件"); }
|
||||
}
|
||||
|
||||
private static void requireShippingLabel(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) throw badRequest("Shipping label file is required");
|
||||
if (file.getSize() > 20L * 1024 * 1024) throw badRequest("Shipping label file exceeds 20 MB");
|
||||
String contentType = Objects.toString(file.getContentType(), "").toLowerCase();
|
||||
if (!(contentType.equals("application/pdf") || contentType.startsWith("image/"))) {
|
||||
throw badRequest("Shipping label must be a PDF or image");
|
||||
}
|
||||
}
|
||||
|
||||
private static String documentUrl(String relabelNo, long fileId, Long itemId) {
|
||||
return "/api/platform/v1/last-leg/relabel-applications/" + java.net.URLEncoder.encode(relabelNo, StandardCharsets.UTF_8) + "/files/" + fileId + "/download?scope=" + (itemId == null ? "TOTAL" : "ITEM&itemId=" + itemId);
|
||||
}
|
||||
@@ -163,6 +232,17 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Transactional
|
||||
public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
boolean requiresOutbound = request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
String deliveryMethod = trim(request.deliveryMethod());
|
||||
if (requiresOutbound && !DELIVERY_METHODS.contains(deliveryMethod)) {
|
||||
throw badRequest("Delivery method must be TRUCK, PICKUP or EXPRESS when carton labels are changed");
|
||||
}
|
||||
if (!requiresOutbound && StringUtils.hasText(deliveryMethod)) {
|
||||
throw badRequest("SKU-only relabel does not create an outbound fulfillment");
|
||||
}
|
||||
if (Boolean.TRUE.equals(request.printShippingLabel()) && !Set.of("EXPRESS", "EXPRESS_PICKUP").contains(deliveryMethod)) {
|
||||
throw badRequest("Shipping-label printing is only available for express delivery");
|
||||
}
|
||||
String requestHash = hash(request);
|
||||
RelabelApplication replay = applications.findKey(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
@@ -223,6 +303,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setLabelDeadline(request.labelDeadline());
|
||||
application.setContactName(request.contactName().trim());
|
||||
application.setContactPhone(request.contactPhone().trim());
|
||||
application.setDeliveryMethod(deliveryMethod);
|
||||
application.setPrintShippingLabel(Boolean.TRUE.equals(request.printShippingLabel()));
|
||||
application.setFulfillmentStatus(requiresOutbound ? "WAIT_RELABEL" : "NOT_REQUIRED");
|
||||
application.setRemark(trim(request.remark()));
|
||||
application.setStatus(PENDING_PROCESSING);
|
||||
application.setItemCount(request.items().size());
|
||||
@@ -285,12 +368,17 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.eq(RelabelStatusHistory::getApplicationId, application.getId())
|
||||
.orderByAsc(RelabelStatusHistory::getCreatedAt));
|
||||
List<LastlegOrder> tailOrders = tailOrders(application);
|
||||
PdaRelabelTask pdaTask = pdaTasks == null ? null : pdaTasks.find(recordCustomerId, application.getId());
|
||||
RelabelResponses.CompletionEvidence evidence = pdaTask == null ? null
|
||||
: new RelabelResponses.CompletionEvidence(pdaTask.getId(), readIds(pdaTask.getCompletionPhotoFileIds()),
|
||||
pdaTask.getCompletionRemark(), pdaTask.getReviewerName(), pdaTask.getReviewRemark(),
|
||||
pdaTask.getReviewedAt());
|
||||
return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(),
|
||||
detailItems.stream().map(this::item).toList(), statusHistory.stream().map(value ->
|
||||
new RelabelResponses.History(value.getFromStatus(), value.getToStatus(), value.getOperatorId(),
|
||||
value.getRemark(), value.getCreatedAt())).toList(), tailOrders.stream().map(value ->
|
||||
new RelabelResponses.TailOrder(value.getId(), value.getOrderNo(), value.getOverseasWarehouseId(),
|
||||
Objects.requireNonNullElse(value.getPackageCount(), 0), value.getCurrentStatus())).toList());
|
||||
Objects.requireNonNullElse(value.getPackageCount(), 0), value.getCurrentStatus())).toList(), evidence);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -313,8 +401,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setStatus(next);
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
if ("COMPLETED".equals(next)) {
|
||||
releaseInventory(customerId, operatorId, application);
|
||||
if (CUSTOMER_CONFIRM_PENDING.equals(next)) {
|
||||
application.setFulfillmentStatus("WAIT_CUSTOMER_CONFIRMATION");
|
||||
applications.updateById(application);
|
||||
} else if ("COMPLETED".equals(next)) {
|
||||
if (!StringUtils.hasText(application.getOutboundOrderNo())) releaseInventory(customerId, operatorId, application);
|
||||
walletCommands.publish("SETTLE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null,
|
||||
"换标完成,结算费用", 2L);
|
||||
application.setWalletStatus("SETTLE_PENDING"); applications.updateById(application);
|
||||
@@ -332,6 +423,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
String orderBefore = order.getCurrentStatus();
|
||||
String mainStatus = STATUSES.contains(orderBefore) ? "PENDING_ASSIGNMENT" : orderBefore;
|
||||
String orderStatus = switch (next) {
|
||||
case CUSTOMER_CONFIRM_PENDING -> mainStatus;
|
||||
case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType())
|
||||
? "PENDING_ASSIGNMENT" : "COMPLETED";
|
||||
case "REJECTED", "CANCELLED" -> "CANCELLED";
|
||||
@@ -350,6 +442,102 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail confirmCompletion(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.CustomerConfirm request) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
String key = request.idempotencyKey().trim();
|
||||
if (key.equals(application.getCustomerConfirmationKey())) return detail(customerId, relabelNo);
|
||||
if (!CUSTOMER_CONFIRM_PENDING.equals(application.getStatus())) {
|
||||
throw conflict("Relabel order is not awaiting customer confirmation");
|
||||
}
|
||||
if (Boolean.TRUE.equals(application.getPrintShippingLabel())
|
||||
&& !StringUtils.hasText(application.getShippingLabelFileId())) {
|
||||
throw conflict("Upload the shipping label before confirming relabel completion");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String before = application.getStatus();
|
||||
application.setStatus(READY_FOR_OUTBOUND);
|
||||
application.setCustomerConfirmationKey(key);
|
||||
application.setCustomerConfirmedAt(now);
|
||||
application.setFulfillmentStatus("TRUCK".equals(application.getDeliveryMethod())
|
||||
? "WAIT_PRE_DISPATCH" : "WAIT_OUTBOUND_CREATION");
|
||||
if (!"TRUCK".equals(application.getDeliveryMethod())) {
|
||||
createDirectOutbound(application, operatorId, now);
|
||||
application.setFulfillmentStatus("OUTBOUND_CREATED");
|
||||
}
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
for (LastlegOrder order : tailOrders(application)) {
|
||||
String orderBefore = order.getCurrentStatus();
|
||||
order.setCurrentStatus("PENDING_ASSIGNMENT");
|
||||
order.setSubOrderStatus("RELABEL:" + application.getFulfillmentStatus());
|
||||
order.setRelabelStatus(READY_FOR_OUTBOUND);
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
if (!Objects.equals(orderBefore, order.getCurrentStatus())) {
|
||||
insertOrderHistory(customerId, order.getId(), orderBefore, order.getCurrentStatus(),
|
||||
request.remark(), now);
|
||||
}
|
||||
}
|
||||
insertHistory(customerId, application.getId(), before, READY_FOR_OUTBOUND, operatorId,
|
||||
request.remark(), now);
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
private void createDirectOutbound(RelabelApplication application, String operatorId, LocalDateTime now) {
|
||||
String key = "RELABEL:" + application.getId() + ":OUTBOUND";
|
||||
OutboundOrder existing = outboundOrders.findByIdempotency(application.getCustomerId(), key);
|
||||
if (existing != null) {
|
||||
application.setOutboundOrderNo(existing.getOutboundNo());
|
||||
return;
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(application.getCustomerId());
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RELABEL_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
|
||||
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); outbound.setOutboundType("RELABEL_OUTBOUND");
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> intValue(item.getNotifiedPackageCount())).sum());
|
||||
outbound.setPalletCount((int) relabelItems.stream().map(RelabelItem::getSourceInventoryPositionId).filter(Objects::nonNull).distinct().count());
|
||||
outbound.setRecipientInfo(application.getDeliveryMethod()); outbound.setIdempotencyKey(key);
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
Map<Long, List<RelabelItem>> byPosition = relabelItems.stream().collect(Collectors.groupingBy(
|
||||
RelabelItem::getSourceInventoryPositionId, LinkedHashMap::new, Collectors.toList()));
|
||||
for (Map.Entry<Long, List<RelabelItem>> entry : byPosition.entrySet()) {
|
||||
InventoryPosition position = inventoryPositions.selectById(entry.getKey());
|
||||
if (position == null || !application.getCustomerId().equals(position.getCustomerId())) {
|
||||
throw conflict("Relabel source inventory position no longer exists");
|
||||
}
|
||||
List<RelabelItem> group = entry.getValue();
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId());
|
||||
item.setCustomerId(application.getCustomerId()); item.setOutboundOrderId(outbound.getId());
|
||||
item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo());
|
||||
item.setPackageCount(group.stream().mapToInt(value -> intValue(value.getNotifiedPackageCount())).sum());
|
||||
item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(group.stream().map(RelabelItem::getCartonMark).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setDestinationWarehouseCode(group.stream().map(RelabelItem::getDestinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setInboundCode(group.stream().map(RelabelItem::getSourceInboundCode).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setOrderRefs(writeOutboundRefs(application.getRelabelNo(), entry.getKey(), group));
|
||||
item.setCreatedAt(now); outboundItems.insert(item);
|
||||
}
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(application.getCustomerId());
|
||||
task.setOutboundOrderId(outbound.getId()); task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId());
|
||||
task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); outboundTasks.insert(task);
|
||||
application.setOutboundOrderNo(outbound.getOutboundNo());
|
||||
}
|
||||
|
||||
private String writeOutboundRefs(String relabelNo, Long positionId, List<RelabelItem> values) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(Map.of("sourceType", "RELABEL_ORDER", "relabelNo", relabelNo,
|
||||
"sourceInventoryPositionId", positionId, "items", values.stream().map(item -> Map.of(
|
||||
"itemId", item.getId(), "cartonMark", Objects.toString(item.getCartonMark(), ""),
|
||||
"destination", Objects.toString(item.getDestinationFbaWarehouseCode(), ""),
|
||||
"packages", intValue(item.getNotifiedPackageCount()))).toList()));
|
||||
} catch (Exception exception) { throw new IllegalStateException("Cannot save relabel outbound snapshot", exception); }
|
||||
}
|
||||
|
||||
private void insertItem(String customerId, long applicationId, SourceInventory sourceInventory,
|
||||
LocalDateTime now) {
|
||||
RelabelRequests.Item source = sourceInventory.request();
|
||||
@@ -391,6 +579,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
Objects.requireNonNullElse(value.getNotifiedPackageCount(), 0), skuCodes,
|
||||
value.getTotalFileId(), value.getTotalFileName(), value.getTotalFileUrl(),
|
||||
value.getChargeAmount(), value.getChargeCurrency(), value.getWalletStatus(), value.getWalletErrorMessage(),
|
||||
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
|
||||
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
||||
value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(),
|
||||
value.getCreatedAt());
|
||||
}
|
||||
|
||||
@@ -474,6 +665,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private record SourceInventory(RelabelRequests.Item request, InventoryBalance balance) {}
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static int intValue(Integer value) { return value == null ? 0 : value; }
|
||||
|
||||
private void releaseInventory(String customerId, String operatorId, RelabelApplication application) {
|
||||
for (RelabelItem item : itemList(application.getId())) {
|
||||
@@ -505,9 +697,6 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
&& (item.skuItems() == null || item.skuItems().isEmpty())) {
|
||||
throw badRequest("SKU label changes require at least one SKU detail");
|
||||
}
|
||||
if ("CARTON_LABEL".equals(type) && !StringUtils.hasText(item.newSkuCode())) {
|
||||
throw badRequest("Carton label changes require the new SKU");
|
||||
}
|
||||
}
|
||||
private static String operationType(RelabelRequests.Item item) {
|
||||
return StringUtils.hasText(item.operationType()) ? item.operationType().trim().toUpperCase() : "CARTON_LABEL";
|
||||
@@ -527,6 +716,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
List.class, RelabelResponses.SkuItem.class));
|
||||
} catch (Exception exception) { throw new IllegalStateException("Cannot read relabel SKU details", exception); }
|
||||
}
|
||||
private List<Long> readIds(String value) {
|
||||
if (!StringUtils.hasText(value)) return List.of();
|
||||
try {
|
||||
return objectMapper.readValue(value, objectMapper.getTypeFactory().constructCollectionType(List.class, Long.class));
|
||||
} catch (Exception exception) { throw conflict("Relabel completion photo snapshot is invalid"); }
|
||||
}
|
||||
private RelabelApplication require(String customerId, String relabelNo) {
|
||||
RelabelApplication value = StringUtils.hasText(customerId)
|
||||
? applications.findNo(customerId, relabelNo)
|
||||
@@ -548,7 +743,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
private static void validateTransition(String from, String to) {
|
||||
boolean allowed = PENDING_PROCESSING.equals(from) && Set.of("PROCESSING", "REJECTED", "CANCELLED").contains(to)
|
||||
|| "PROCESSING".equals(from) && Set.of("COMPLETED", "REJECTED", "CANCELLED").contains(to);
|
||||
|| "PROCESSING".equals(from) && Set.of(CUSTOMER_CONFIRM_PENDING, "REJECTED", "CANCELLED").contains(to)
|
||||
|| CUSTOMER_CONFIRM_PENDING.equals(from) && Set.of(READY_FOR_OUTBOUND, "REJECTED", "CANCELLED").contains(to)
|
||||
|| READY_FOR_OUTBOUND.equals(from) && Set.of("COMPLETED", "CANCELLED").contains(to);
|
||||
if (!allowed) throw conflict("Relabel status cannot change from " + from + " to " + to);
|
||||
}
|
||||
private String hash(Object value) {
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Keeps relabel ownership and its direct-outbound snapshot aligned after a physical pallet split. */
|
||||
@Service
|
||||
public class RelabelPalletMutationService {
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private OutboundOrderMapper outboundOrders;
|
||||
@Resource private OutboundPalletItemMapper outboundItems;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
public void moveSelectedItems(RelabelApplication application,
|
||||
Map<Long, List<RelabelItem>> itemsBySourcePosition,
|
||||
Map<Long, InventoryPosition> targetBySourcePosition) {
|
||||
for (Map.Entry<Long, List<RelabelItem>> entry : itemsBySourcePosition.entrySet()) {
|
||||
InventoryPosition target = targetBySourcePosition.get(entry.getKey());
|
||||
if (target == null) throw conflict("换标拆托目标库存位置不存在");
|
||||
for (RelabelItem item : entry.getValue()) {
|
||||
item.setSourceInventoryPositionId(target.getId());
|
||||
relabelItems.updateById(item);
|
||||
}
|
||||
}
|
||||
syncDirectOutbound(application, itemsBySourcePosition, targetBySourcePosition);
|
||||
}
|
||||
|
||||
private void syncDirectOutbound(RelabelApplication application,
|
||||
Map<Long, List<RelabelItem>> itemsBySourcePosition,
|
||||
Map<Long, InventoryPosition> targetBySourcePosition) {
|
||||
if (application.getOutboundOrderNo() == null || application.getOutboundOrderNo().isBlank()) return;
|
||||
OutboundOrder outbound = outboundOrders.lockByNo(application.getCustomerId(), application.getOutboundOrderNo());
|
||||
if (outbound == null || !"RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
throw conflict("换标出库单不存在或类型不正确");
|
||||
}
|
||||
if (!"WAIT_PICK".equals(outbound.getStatus())) throw conflict("换标出库已开始拣货,不能再拆托");
|
||||
List<OutboundPalletItem> rows = new ArrayList<>(outboundItems.findByOutbound(
|
||||
application.getCustomerId(), outbound.getId()));
|
||||
for (Map.Entry<Long, List<RelabelItem>> entry : itemsBySourcePosition.entrySet()) {
|
||||
List<RelabelItem> selected = entry.getValue();
|
||||
LinkedHashSet<String> selectedMarks = selected.stream().map(RelabelItem::getCartonMark)
|
||||
.filter(Objects::nonNull).map(String::trim)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
long selectedPackages = selected.stream().mapToLong(item -> value(item.getNotifiedPackageCount())).sum();
|
||||
OutboundPalletItem sourceRow = rows.stream().filter(row -> value(row.getPickedPackageCount()) == 0)
|
||||
.filter(row -> cartonMarks(row).containsAll(selectedMarks)).findFirst()
|
||||
.orElseThrow(() -> conflict("换标出库箱唛快照与拆托任务不一致"));
|
||||
LinkedHashSet<String> remaining = cartonMarks(sourceRow);
|
||||
remaining.removeAll(selectedMarks);
|
||||
InventoryPosition target = targetBySourcePosition.get(entry.getKey());
|
||||
if (remaining.isEmpty()) {
|
||||
sourceRow.setWarehousePalletId(target.getPalletId()); sourceRow.setPalletNo(target.getPalletNo());
|
||||
sourceRow.setOrderRefs(outboundRefs(application.getRelabelNo(), target.getId(), selected));
|
||||
outboundItems.updateById(sourceRow);
|
||||
continue;
|
||||
}
|
||||
int remainingPackages = value(sourceRow.getPackageCount()) - Math.toIntExact(selectedPackages);
|
||||
if (remainingPackages <= 0) throw conflict("换标出库剩余箱唛与箱数快照不一致");
|
||||
sourceRow.setPackageCount(remainingPackages); sourceRow.setCartonMarks(String.join(",", remaining));
|
||||
List<RelabelItem> remainingItems = relabelItems.selectList(new LambdaQueryWrapper<RelabelItem>()
|
||||
.eq(RelabelItem::getApplicationId, application.getId())
|
||||
.eq(RelabelItem::getCustomerId, application.getCustomerId())
|
||||
.eq(RelabelItem::getSourceInventoryPositionId, entry.getKey()));
|
||||
sourceRow.setOrderRefs(outboundRefs(application.getRelabelNo(), entry.getKey(), remainingItems));
|
||||
outboundItems.updateById(sourceRow);
|
||||
|
||||
OutboundPalletItem targetRow = new OutboundPalletItem();
|
||||
targetRow.setId(IdWorker.getId()); targetRow.setCustomerId(sourceRow.getCustomerId());
|
||||
targetRow.setOutboundOrderId(sourceRow.getOutboundOrderId()); targetRow.setWarehousePalletId(target.getPalletId());
|
||||
targetRow.setPalletNo(target.getPalletNo()); targetRow.setDestinationWarehouseId(sourceRow.getDestinationWarehouseId());
|
||||
targetRow.setDestinationWarehouseCode(sourceRow.getDestinationWarehouseCode());
|
||||
targetRow.setDestinationWarehouseName(sourceRow.getDestinationWarehouseName());
|
||||
targetRow.setPackageCount(Math.toIntExact(selectedPackages)); targetRow.setPickedPackageCount(0);
|
||||
targetRow.setStatus("WAIT_PICK"); targetRow.setSkuCode(sourceRow.getSkuCode());
|
||||
targetRow.setCartonMarks(String.join(",", selectedMarks)); targetRow.setInboundCode(sourceRow.getInboundCode());
|
||||
targetRow.setOrderRefs(outboundRefs(application.getRelabelNo(), target.getId(), selected));
|
||||
targetRow.setCreatedAt(LocalDateTime.now()); outboundItems.insert(targetRow); rows.add(targetRow);
|
||||
}
|
||||
outbound.setPalletCount((int) rows.stream().map(OutboundPalletItem::getWarehousePalletId)
|
||||
.filter(Objects::nonNull).distinct().count());
|
||||
outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(outbound);
|
||||
}
|
||||
|
||||
private static LinkedHashSet<String> cartonMarks(OutboundPalletItem item) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
if (item.getCartonMarks() == null) return result;
|
||||
for (String value : item.getCartonMarks().split(",")) if (!value.isBlank()) result.add(value.trim());
|
||||
return result;
|
||||
}
|
||||
|
||||
private String outboundRefs(String relabelNo, Long positionId, List<RelabelItem> items) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(Map.of("sourceType", "RELABEL_ORDER", "relabelNo", relabelNo,
|
||||
"sourceInventoryPositionId", positionId, "items", items.stream().map(item -> Map.of(
|
||||
"itemId", item.getId(), "cartonMark", Objects.toString(item.getCartonMark(), ""),
|
||||
"destination", Objects.toString(item.getDestinationFbaWarehouseCode(), ""),
|
||||
"packages", value(item.getNotifiedPackageCount()))).toList()));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("无法保存换标出库托盘快照", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static int value(Integer value) { return value == null ? 0 : value; }
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
+2
@@ -13,6 +13,8 @@ import java.util.List;
|
||||
public final class ReturnOrderRequests {
|
||||
private ReturnOrderRequests() { }
|
||||
public record Create(@NotBlank @Size(max = 128) String overseasWarehouseId,
|
||||
@Size(max = 128) String warehouseContactName,
|
||||
@Size(max = 64) String warehouseContactPhone,
|
||||
@Size(max = 128) String customerReferenceNo,
|
||||
boolean needRelabel,
|
||||
@Size(max = 1000) String remark,
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ import java.util.List;
|
||||
|
||||
public final class ReturnOrderResponses {
|
||||
private ReturnOrderResponses() { }
|
||||
public record Summary(String returnNo, String lastlegOrderNo, String overseasWarehouseId, String customerReferenceNo, String status,
|
||||
public record Summary(String returnNo, String lastlegOrderNo, String overseasWarehouseId,
|
||||
String warehouseContactName, String warehouseContactPhone, String customerReferenceNo, String status,
|
||||
int skuTypes, long expectedQuantity, long receivedQuantity, boolean needRelabel,
|
||||
String relabelNo, String relabelStatus, LocalDateTime createdAt) { }
|
||||
public record Item(String skuCode, String productNameZh, long expectedQuantity,
|
||||
|
||||
@@ -14,6 +14,8 @@ public class ReturnOrder {
|
||||
private String customerId;
|
||||
private String returnNo;
|
||||
private String overseasWarehouseId;
|
||||
private String warehouseContactName;
|
||||
private String warehouseContactPhone;
|
||||
private String customerReferenceNo;
|
||||
private Boolean needRelabel;
|
||||
private Long lastlegOrderId;
|
||||
|
||||
+3
-1
@@ -77,6 +77,8 @@ public class ReturnReceiptService {
|
||||
ReturnOrder order = new ReturnOrder();
|
||||
order.setId(IdWorker.getId()); order.setCustomerId(customerId);
|
||||
order.setOverseasWarehouseId(text(request.overseasWarehouseId()));
|
||||
order.setWarehouseContactName(text(request.warehouseContactName()));
|
||||
order.setWarehouseContactPhone(text(request.warehouseContactPhone()));
|
||||
order.setReturnNo(numbers.next("RETURN", order.getOverseasWarehouseId(), order.getId()));
|
||||
order.setCustomerReferenceNo(text(request.customerReferenceNo())); order.setNeedRelabel(request.needRelabel()); order.setStatus(PENDING_RECEIPT);
|
||||
order.setExpectedSkuTypes(skuCodes.size()); order.setExpectedQuantity(expected); order.setReceivedQuantity(0L);
|
||||
@@ -293,7 +295,7 @@ public class ReturnReceiptService {
|
||||
order.getStatus(), completed, value(order.getExpectedQuantity()), value(order.getReceivedQuantity()), receipt.getSignedAt());
|
||||
}
|
||||
private ReturnOrderResponses.Summary summary(ReturnOrder order) { return new ReturnOrderResponses.Summary(order.getReturnNo(), order.getLastlegOrderNo(),
|
||||
order.getOverseasWarehouseId(), order.getCustomerReferenceNo(), order.getStatus(),
|
||||
order.getOverseasWarehouseId(), order.getWarehouseContactName(), order.getWarehouseContactPhone(), order.getCustomerReferenceNo(), order.getStatus(),
|
||||
order.getExpectedSkuTypes() == null ? 0 : order.getExpectedSkuTypes(), value(order.getExpectedQuantity()),
|
||||
value(order.getReceivedQuantity()), Boolean.TRUE.equals(order.getNeedRelabel()), order.getRelabelNo(), order.getRelabelStatus(), order.getCreatedAt()); }
|
||||
private ReturnOrderResponses.Item item(ReturnOrderItem item) { return new ReturnOrderResponses.Item(item.getSkuCode(),
|
||||
|
||||
+4
-4
@@ -17,16 +17,16 @@ public final class WarehouseTransferRequests {
|
||||
private WarehouseTransferRequests() {}
|
||||
|
||||
public record Create(
|
||||
@NotBlank @Size(max = 128) String inboundCode,
|
||||
@NotEmpty @Size(max = 200) List<@NotNull Long> sourceLastlegOrderIds,
|
||||
@Size(max = 128) String inboundCode,
|
||||
@Size(max = 200) List<@NotNull Long> sourceLastlegOrderIds,
|
||||
@NotBlank @Size(max = 128) String operatingWarehouseId,
|
||||
@NotBlank @Size(max = 128) String fbaWarehouseCode,
|
||||
@Size(max = 128) String fbaWarehouseCode,
|
||||
@NotBlank @Size(max = 1000) String deliveryAddress,
|
||||
@Positive int packageCount,
|
||||
@NotNull @DecimalMin(value = "0.0001") BigDecimal volumeCbm,
|
||||
@NotBlank @Size(max = 64) String deliveryMethod,
|
||||
@NotNull LocalDateTime estimatedArrivalAt,
|
||||
@NotBlank @Size(max = 128) String fbaShipmentNo,
|
||||
@Size(max = 128) String fbaShipmentNo,
|
||||
@Size(max = 255) String trackingNo,
|
||||
@Size(max = 2000) String fbaBoxNos,
|
||||
@Size(max = 1000) String remark,
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
public final class WarehouseTransferResponses {
|
||||
private WarehouseTransferResponses() {}
|
||||
public record Item(String skuCode, String productNameZh, int packageCount, String shippingMark) {}
|
||||
public record Carton(String cartonMark, int cartonIndex, int totalCartons, String status) {}
|
||||
public record SourceOrder(Long lastlegOrderId, String lastlegOrderNo, Long inboundOrderId, String inboundCode) {}
|
||||
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
|
||||
Long palletId, String palletNo, int packageCount, String status,
|
||||
@@ -21,6 +22,6 @@ public final class WarehouseTransferResponses {
|
||||
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
|
||||
int packageCount, String deliveryMethod, LocalDateTime estimatedArrivalAt,
|
||||
String fbaShipmentNo, String trackingNo, String fbaBoxNos, String remark,
|
||||
List<SourceOrder> sourceOrders, List<Item> items, List<Allocation> allocations,
|
||||
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons, List<Allocation> allocations,
|
||||
LocalDateTime outboundAt, LocalDateTime createdAt) {}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.transfer.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_warehouse_transfer_carton")
|
||||
public class WarehouseTransferCarton {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long transferOrderId;
|
||||
private Long transferItemId;
|
||||
private String customerId;
|
||||
private String cartonMark;
|
||||
private Integer cartonIndex;
|
||||
private Integer totalCartons;
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton;
|
||||
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 WarehouseTransferCartonMapper extends BaseMapper<WarehouseTransferCarton> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_carton WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY carton_index")
|
||||
List<WarehouseTransferCarton> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
}
|
||||
+97
-71
@@ -5,11 +5,13 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegInboundOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegInboundOrderMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
|
||||
@@ -17,22 +19,19 @@ import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelStatusHistory;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferItem;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferReceipt;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferSourceOrder;
|
||||
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferItemMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
|
||||
@@ -61,7 +60,6 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class WarehouseTransferService {
|
||||
public static final String ORDER_TYPE = "WAREHOUSE_TRANSFER";
|
||||
public static final String RETURN_RELABEL_ORDER_TYPE = "RETURN_RELABEL";
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
public static final String APPROVED = "APPROVED";
|
||||
public static final String REJECTED = "REJECTED";
|
||||
@@ -73,16 +71,16 @@ public class WarehouseTransferService {
|
||||
|
||||
@Resource private WarehouseTransferOrderMapper transfers;
|
||||
@Resource private WarehouseTransferItemMapper transferItems;
|
||||
@Resource private WarehouseTransferCartonMapper transferCartons;
|
||||
@Resource private WarehouseTransferSourceOrderMapper sourceOrders;
|
||||
@Resource private WarehouseTransferInventoryAllocationMapper inventoryAllocations;
|
||||
@Resource private WarehouseTransferReceiptMapper transferReceipts;
|
||||
@Resource private InboundSnapshotMapper inboundSnapshots;
|
||||
@Resource private LastlegInboundOrderMapper inboundOrders;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelStatusHistoryMapper relabelHistories;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private InventoryCommandMapper inventoryCommands;
|
||||
@@ -105,30 +103,47 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, replay.getTransferNo());
|
||||
}
|
||||
|
||||
String inboundCode = request.inboundCode().trim();
|
||||
Long inboundOrderId = inboundSnapshots.findId(customerId, inboundCode);
|
||||
if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode);
|
||||
List<Long> selectedIds = request.sourceLastlegOrderIds().stream().distinct().toList();
|
||||
List<LastlegOrder> selectedOrders = orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds));
|
||||
if (selectedOrders.size() != selectedIds.size()) {
|
||||
throw notFound("One or more selected customer orders do not exist in the current tenant");
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long transferId = IdWorker.getId();
|
||||
String transferNo = orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), transferId);
|
||||
LastlegOrder order = createTailOrder(customerId, inboundOrderId, transferNo, request, requestHash, now);
|
||||
String requestedInboundCode = trim(request.inboundCode());
|
||||
List<Long> selectedIds = request.sourceLastlegOrderIds() == null
|
||||
? List.of() : request.sourceLastlegOrderIds().stream().distinct().toList();
|
||||
boolean existingInventoryTransfer = StringUtils.hasText(requestedInboundCode) || !selectedIds.isEmpty();
|
||||
if (existingInventoryTransfer && (!StringUtils.hasText(requestedInboundCode) || selectedIds.isEmpty())) {
|
||||
throw badRequest("Existing inventory transfer requires both inboundCode and sourceLastlegOrderIds");
|
||||
}
|
||||
|
||||
Long inboundOrderId;
|
||||
String inboundCode;
|
||||
List<LastlegOrder> selectedOrders;
|
||||
if (existingInventoryTransfer) {
|
||||
inboundCode = requestedInboundCode;
|
||||
inboundOrderId = inboundSnapshots.findId(customerId, inboundCode);
|
||||
if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode);
|
||||
selectedOrders = orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds));
|
||||
if (selectedOrders.size() != selectedIds.size()) {
|
||||
throw notFound("One or more selected customer orders do not exist in the current tenant");
|
||||
}
|
||||
} else {
|
||||
inboundCode = transferNo;
|
||||
inboundOrderId = createInboundSnapshot(customerId, transferId, transferNo, request, requestHash, now);
|
||||
selectedOrders = List.of();
|
||||
}
|
||||
LastlegOrder order = createTailOrder(customerId, inboundOrderId, inboundCode, transferNo,
|
||||
request, requestHash, now);
|
||||
|
||||
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
|
||||
transfer.setId(transferId); transfer.setCustomerId(customerId); transfer.setTransferNo(transferNo);
|
||||
transfer.setInboundOrderId(inboundOrderId); transfer.setInboundCode(inboundCode);
|
||||
transfer.setLastlegOrderId(order.getId()); transfer.setLastlegOrderNo(order.getOrderNo());
|
||||
transfer.setOperatingWarehouseId(request.operatingWarehouseId().trim());
|
||||
transfer.setFbaWarehouseCode(request.fbaWarehouseCode().trim());
|
||||
transfer.setFbaWarehouseCode(trim(request.fbaWarehouseCode()));
|
||||
transfer.setDeliveryAddress(request.deliveryAddress().trim()); transfer.setPackageCount(request.packageCount());
|
||||
transfer.setDeliveryMethod(request.deliveryMethod().trim()); transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
|
||||
transfer.setFbaShipmentNo(request.fbaShipmentNo().trim()); transfer.setTrackingNo(trim(request.trackingNo()));
|
||||
transfer.setFbaShipmentNo(trim(request.fbaShipmentNo()));
|
||||
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim()) ? trim(request.trackingNo()) : null);
|
||||
transfer.setFbaBoxNos(trim(request.fbaBoxNos())); transfer.setRemark(trim(request.remark()));
|
||||
transfer.setNeedRelabel(request.needRelabel()); transfer.setStatus(PENDING_REVIEW);
|
||||
transfer.setIdempotencyKey(key); transfer.setRequestHash(requestHash); transfer.setCreatedBy(trim(operatorId));
|
||||
@@ -149,14 +164,19 @@ public class WarehouseTransferService {
|
||||
sourceOrders.insert(link);
|
||||
}
|
||||
|
||||
int cartonIndex = 0;
|
||||
for (int index = 0; index < request.items().size(); index++) {
|
||||
WarehouseTransferRequests.Item source = request.items().get(index);
|
||||
insertTransferItem(customerId, transferId, source, now);
|
||||
long itemId = insertTransferItem(customerId, transferId, source, now);
|
||||
for (int itemCarton = 0; itemCarton < source.packageCount(); itemCarton++) {
|
||||
cartonIndex++;
|
||||
insertTransferCarton(customerId, transferId, itemId, transferNo,
|
||||
cartonIndex, request.packageCount(), now);
|
||||
}
|
||||
insertOrderDetail(customerId, order.getId(), transferNo, index, source, request, now);
|
||||
}
|
||||
insertOrderHistory(customerId, order.getId(), null, PENDING_REVIEW, transferNo,
|
||||
"Warehouse transfer submitted for review", now);
|
||||
if (request.needRelabel()) createRelabel(customerId, operatorId, transfer, order, request, now);
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@@ -200,6 +220,9 @@ public class WarehouseTransferService {
|
||||
.eq(WarehouseTransferSourceOrder::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferSourceOrder::getId))
|
||||
.stream().map(value -> new WarehouseTransferResponses.SourceOrder(value.getSourceLastlegOrderId(),
|
||||
value.getSourceLastlegOrderNo(), value.getInboundOrderId(), value.getInboundCode())).toList();
|
||||
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId())
|
||||
.stream().map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(),
|
||||
value.getCartonIndex(), value.getTotalCartons(), value.getStatus())).toList();
|
||||
Map<Long, WarehouseTransferReceipt> receiptByAllocation = transferReceipts.selectList(
|
||||
new LambdaQueryWrapper<WarehouseTransferReceipt>().eq(WarehouseTransferReceipt::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferReceipt::getTransferOrderId, transfer.getId()))
|
||||
@@ -230,7 +253,7 @@ public class WarehouseTransferService {
|
||||
relabelStatus,
|
||||
transfer.getOperatingWarehouseId(), transfer.getFbaWarehouseCode(), transfer.getDeliveryAddress(),
|
||||
transfer.getPackageCount(), transfer.getDeliveryMethod(), transfer.getEstimatedArrivalAt(), transfer.getFbaShipmentNo(),
|
||||
transfer.getTrackingNo(), transfer.getFbaBoxNos(), transfer.getRemark(), sources, items, allocations,
|
||||
transfer.getTrackingNo(), transfer.getFbaBoxNos(), transfer.getRemark(), sources, items, cartons, allocations,
|
||||
transfer.getOutboundAt(), transfer.getCreatedAt());
|
||||
}
|
||||
|
||||
@@ -254,9 +277,15 @@ 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) ? PENDING_ASSIGNMENT : "CANCELLED";
|
||||
boolean incomingTransit = transfer.getTransferNo().equals(transfer.getInboundCode());
|
||||
String orderStatus = APPROVED.equals(next)
|
||||
? (incomingTransit
|
||||
? ("WAREHOUSE_PICKUP".equals(transfer.getDeliveryMethod()) ? "WAIT_PICKUP" : "WAIT_DELIVERY")
|
||||
: PENDING_ASSIGNMENT)
|
||||
: "CANCELLED";
|
||||
order.setCurrentStatus(orderStatus);
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + next);
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + next + ";RELABEL:"
|
||||
+ (StringUtils.hasText(order.getRelabelStatus()) ? order.getRelabelStatus() : "NOT_REQUIRED"));
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
if (!java.util.Objects.equals(before, orderStatus)) {
|
||||
insertOrderHistory(customerId, order.getId(), before, orderStatus, transferNo,
|
||||
@@ -461,20 +490,41 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
private LastlegOrder createTailOrder(String customerId, Long inboundOrderId, String transferNo,
|
||||
private Long createInboundSnapshot(String customerId, long inboundOrderId, String transferNo,
|
||||
WarehouseTransferRequests.Create request, String requestHash,
|
||||
LocalDateTime now) {
|
||||
LastlegInboundOrder inbound = new LastlegInboundOrder();
|
||||
String sourceId = "WAREHOUSE_TRANSFER:" + transferNo;
|
||||
inbound.setId(inboundOrderId); inbound.setCustomerId(customerId); inbound.setSourceRegion("CUSTOMER");
|
||||
inbound.setSourceInboundId(sourceId); inbound.setRootInboundId(sourceId);
|
||||
inbound.setSourceOrderId(transferNo); inbound.setSourceOrderNo(transferNo); inbound.setRootOrderId(transferNo);
|
||||
inbound.setInboundCode(transferNo); inbound.setDestinationWarehouseId(request.operatingWarehouseId().trim());
|
||||
inbound.setDestinationType("OVERSEAS"); inbound.setPackageCount(request.packageCount());
|
||||
inbound.setPalletCount(0); inbound.setVolumeCbm(request.volumeCbm());
|
||||
inbound.setSourceEventId(sourceId); inbound.setSourceLoadingPlanId(ORDER_TYPE);
|
||||
inbound.setSourceLoadingPlanVersion(1L); inbound.setHandoverVersion(1L);
|
||||
inbound.setPayloadHash("sha256:" + requestHash); inbound.setStatus(PENDING_REVIEW);
|
||||
inbound.setExceptionFlag("NONE"); inbound.setCreatedAt(now); inbound.setUpdatedAt(now);
|
||||
inboundOrders.insert(inbound);
|
||||
return inbound.getId();
|
||||
}
|
||||
|
||||
private LastlegOrder createTailOrder(String customerId, Long inboundOrderId, String inboundCode, String transferNo,
|
||||
WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(IdWorker.getId()); order.setOrderNo(orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), order.getId())); order.setCustomerId(customerId);
|
||||
order.setOrderType(request.needRelabel() ? RETURN_RELABEL_ORDER_TYPE : ORDER_TYPE);
|
||||
order.setInboundCode(request.inboundCode().trim()); order.setSourceRegion("CUSTOMER");
|
||||
order.setOrderType(ORDER_TYPE);
|
||||
order.setInboundCode(inboundCode); order.setSourceRegion("CUSTOMER");
|
||||
order.setSourceInboundId("WAREHOUSE_TRANSFER:" + transferNo); order.setRootInboundId(String.valueOf(inboundOrderId));
|
||||
order.setSourceOrderNo(transferNo); order.setOverseasWarehouseId(request.operatingWarehouseId().trim());
|
||||
order.setFbaWarehouseCode(request.fbaWarehouseCode().trim()); order.setPackageCount(request.packageCount());
|
||||
order.setFbaWarehouseCode(trim(request.fbaWarehouseCode())); order.setPackageCount(request.packageCount());
|
||||
order.setVolumeCbm(request.volumeCbm());
|
||||
order.setContainerNo(transferNo);
|
||||
order.setPalletCount(0); order.setPickedPackageCount(0); order.setShippedPackageCount(0);
|
||||
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
|
||||
order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + PENDING_REVIEW);
|
||||
order.setCurrentStatus(PENDING_REVIEW);
|
||||
order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + PENDING_REVIEW + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setCustomerRemark(trim(request.remark()));
|
||||
order.setSourceEventId("WAREHOUSE_TRANSFER:" + transferNo); order.setSourceLoadingPlanId(ORDER_TYPE);
|
||||
order.setSourcePayloadHash("sha256:" + requestHash); order.setCreatedAt(now); order.setUpdatedAt(now);
|
||||
@@ -482,13 +532,24 @@ public class WarehouseTransferService {
|
||||
return order;
|
||||
}
|
||||
|
||||
private void insertTransferItem(String customerId, long transferId, WarehouseTransferRequests.Item source,
|
||||
private long insertTransferItem(String customerId, long transferId, WarehouseTransferRequests.Item source,
|
||||
LocalDateTime now) {
|
||||
WarehouseTransferItem item = new WarehouseTransferItem();
|
||||
item.setId(IdWorker.getId()); item.setTransferOrderId(transferId); item.setCustomerId(customerId);
|
||||
item.setSkuCode(source.skuCode().trim()); item.setProductNameZh(source.productNameZh().trim());
|
||||
item.setPackageCount(source.packageCount()); item.setShippingMark(trim(source.shippingMark())); item.setCreatedAt(now);
|
||||
transferItems.insert(item);
|
||||
return item.getId();
|
||||
}
|
||||
|
||||
private void insertTransferCarton(String customerId, long transferId, long transferItemId,
|
||||
String transferNo, int cartonIndex, int totalCartons,
|
||||
LocalDateTime now) {
|
||||
WarehouseTransferCarton carton = new WarehouseTransferCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setTransferOrderId(transferId); carton.setTransferItemId(transferItemId);
|
||||
carton.setCustomerId(customerId); carton.setCartonMark(transferNo + "#" + totalCartons + "-" + cartonIndex);
|
||||
carton.setCartonIndex(cartonIndex); carton.setTotalCartons(totalCartons); carton.setStatus("EXPECTED");
|
||||
carton.setCreatedAt(now); carton.setUpdatedAt(now); transferCartons.insert(carton);
|
||||
}
|
||||
|
||||
private void insertOrderDetail(String customerId, long orderId, String transferNo, int index, WarehouseTransferRequests.Item source,
|
||||
@@ -497,48 +558,13 @@ public class WarehouseTransferService {
|
||||
detail.setCustomerId(customerId);
|
||||
detail.setOrderId(orderId); detail.setSourceInboundDetailId(transferNo + ":" + index);
|
||||
detail.setSkuCode(source.skuCode().trim()); detail.setProductNameZh(source.productNameZh().trim());
|
||||
detail.setPackageCount(source.packageCount()); detail.setDestinationWarehouseCode(request.fbaWarehouseCode().trim());
|
||||
detail.setFbaTrackingNo(trim(request.trackingNo())); detail.setShipmentNo(request.fbaShipmentNo().trim());
|
||||
detail.setPackageCount(source.packageCount()); detail.setDestinationWarehouseCode(trim(request.fbaWarehouseCode()));
|
||||
detail.setFbaTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim())
|
||||
? trim(request.trackingNo()) : null);
|
||||
detail.setShipmentNo(trim(request.fbaShipmentNo()));
|
||||
detail.setShippingMark(trim(source.shippingMark())); orderDetails.insert(detail);
|
||||
}
|
||||
|
||||
private void createRelabel(String customerId, String operatorId, WarehouseTransferOrder transfer, LastlegOrder order,
|
||||
WarehouseTransferRequests.Create request, LocalDateTime now) {
|
||||
long applicationId = IdWorker.getId();
|
||||
String relabelNo = orderNoGenerator.next("RELABEL", request.operatingWarehouseId(), applicationId);
|
||||
RelabelApplication application = new RelabelApplication();
|
||||
application.setId(applicationId); application.setCustomerId(customerId); application.setRelabelNo(relabelNo);
|
||||
application.setLastlegOrderId(order.getId()); application.setCustomerOrderNo(transfer.getTransferNo());
|
||||
application.setOverseasWarehouseId(request.operatingWarehouseId().trim()); application.setLabelDeadline(request.estimatedArrivalAt());
|
||||
application.setContactName(StringUtils.hasText(operatorId) ? operatorId.trim() : "SYSTEM");
|
||||
application.setContactPhone("N/A"); application.setRemark(trim(request.remark()));
|
||||
application.setStatus(RelabelApplicationService.PENDING_PROCESSING); application.setItemCount(request.items().size());
|
||||
application.setTotalPackageCount(request.packageCount()); application.setNotifiedPackageCount(request.packageCount());
|
||||
application.setIdempotencyKey("WAREHOUSE_TRANSFER:" + request.idempotencyKey().trim());
|
||||
application.setRequestHash(hash(request)); application.setCreatedBy(trim(operatorId));
|
||||
application.setCreatedAt(now); application.setUpdatedAt(now); relabelApplications.insert(application);
|
||||
|
||||
for (WarehouseTransferRequests.Item source : request.items()) {
|
||||
RelabelItem item = new RelabelItem();
|
||||
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId);
|
||||
item.setSkuCode(source.skuCode().trim()); item.setProductNameZh(source.productNameZh().trim());
|
||||
item.setTotalPackageCount(source.packageCount()); item.setNotifiedPackageCount(source.packageCount());
|
||||
item.setShipmentNo(request.fbaShipmentNo().trim()); item.setTrackingCode(trim(request.trackingNo()));
|
||||
item.setNewSkuCode(source.skuCode().trim()); item.setLabelRequirement("WAREHOUSE_TRANSFER_RELABEL");
|
||||
item.setCreatedAt(now); relabelItems.insert(item);
|
||||
}
|
||||
RelabelStatusHistory history = new RelabelStatusHistory();
|
||||
history.setId(IdWorker.getId()); history.setCustomerId(customerId); history.setApplicationId(applicationId);
|
||||
history.setToStatus(RelabelApplicationService.PENDING_PROCESSING); history.setOperatorId(trim(operatorId));
|
||||
history.setRemark("Created from warehouse transfer " + transfer.getTransferNo()); history.setCreatedAt(now);
|
||||
relabelHistories.insert(history);
|
||||
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);
|
||||
}
|
||||
|
||||
private void insertOrderHistory(String customerId, long orderId, String fromStatus, String toStatus,
|
||||
String transferNo, String remark, LocalDateTime now) {
|
||||
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
|
||||
|
||||
Reference in New Issue
Block a user