优化提交
This commit is contained in:
+7
@@ -77,7 +77,14 @@ public class LastlegOrderController {
|
||||
public ApiResponse<PageResult<LastlegOrderSummary>> list(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@RequestParam(required = false) String relabelNo,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
return ApiResponse.success(orderService.page(platformScope(user, customerId), query, relabelNo));
|
||||
}
|
||||
|
||||
/** Compatibility overload used by non-Spring callers and existing contract tests. */
|
||||
public ApiResponse<PageResult<LastlegOrderSummary>> list(
|
||||
AuthenticatedUser user, String customerId, LastlegOrderRequests.Query query) {
|
||||
return ApiResponse.success(orderService.page(platformScope(user, customerId), query));
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
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 OutboundOrderMapper extends BaseMapper<OutboundOrder> {
|
||||
@@ -22,4 +23,11 @@ public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> {
|
||||
OutboundOrder lockByNo(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE outbound_no=#{outboundNo} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockGlobalByNo(@Param("outboundNo") String outboundNo);
|
||||
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{warehouseId} AND outbound_type='RELABEL_OUTBOUND' " +
|
||||
"AND outbound_no LIKE CONCAT('%',#{keyword},'%') ORDER BY created_at DESC,id DESC LIMIT 100")
|
||||
List<OutboundOrder> listRelabelOutbounds(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("keyword") String keyword);
|
||||
}
|
||||
|
||||
+26
-5
@@ -57,6 +57,8 @@ 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 com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -86,6 +88,7 @@ public class DeliveryTransferService {
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryCommandService inventoryCommands;
|
||||
@Resource private WarehouseTransferPalletMapper warehouseTransferPallets;
|
||||
@Resource private DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Resource private DropshipCartonMapper dropshipCartons;
|
||||
@Resource private DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@@ -175,10 +178,8 @@ public class DeliveryTransferService {
|
||||
item.setDestinationWarehouseCode(source.getDestinationWarehouseCode());
|
||||
item.setDestinationWarehouseName(source.getDestinationWarehouseName());
|
||||
item.setPackageCount(Objects.requireNonNullElse(source.getPackageCount(), 0)); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
List<String> requestedMarks = request.palletCartonMarks() == null ? List.of() : request.palletCartonMarks().getOrDefault(source.getPalletNo(), List.of());
|
||||
if (!requestedMarks.isEmpty()) {
|
||||
if (requestedMarks.size() != Objects.requireNonNullElse(source.getPackageCount(), 0)) throw conflict("Selected carton mark count must equal pallet package count");
|
||||
item.setCartonMarks(String.join(",", requestedMarks.stream().distinct().toList()));
|
||||
if (StringUtils.hasText(source.getCartonMarks())) {
|
||||
item.setCartonMarks(source.getCartonMarks());
|
||||
} else if (source.getWarehousePalletId() != null) {
|
||||
String marks = inboundCartons.listByPallet(customerId, source.getWarehousePalletId()).stream()
|
||||
.map(c -> c.getCartonMark()).filter(StringUtils::hasText).distinct().collect(java.util.stream.Collectors.joining(","));
|
||||
@@ -802,7 +803,27 @@ public class DeliveryTransferService {
|
||||
.stream().map(LastlegOrder::getLxCode).filter(StringUtils::hasText)
|
||||
.map(String::trim).distinct().collect(java.util.stream.Collectors.joining(","));
|
||||
}
|
||||
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) { List<Long> ids = items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).distinct().toList(); if (ids.isEmpty()) return BigDecimal.ZERO; return tailOrderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>().eq(LastlegOrderDetail::getCustomerId, customerId).in(LastlegOrderDetail::getOrderId, ids)).stream().map(LastlegOrderDetail::getGrossWeightKg).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); }
|
||||
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) {
|
||||
List<Long> palletIds = items.stream().map(DispatchItem::getWarehousePalletId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
List<WarehouseTransferPallet> transferPallets = palletIds.isEmpty() ? List.of()
|
||||
: warehouseTransferPallets.selectList(new LambdaQueryWrapper<WarehouseTransferPallet>()
|
||||
.eq(WarehouseTransferPallet::getCustomerId, customerId)
|
||||
.in(WarehouseTransferPallet::getWarehousePalletId, palletIds));
|
||||
Set<Long> measuredPalletIds = transferPallets.stream()
|
||||
.map(WarehouseTransferPallet::getWarehousePalletId).collect(java.util.stream.Collectors.toSet());
|
||||
BigDecimal measuredWeight = transferPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
List<Long> orderIds = items.stream()
|
||||
.filter(item -> item.getWarehousePalletId() == null || !measuredPalletIds.contains(item.getWarehousePalletId()))
|
||||
.map(DispatchItem::getOrderId).filter(Objects::nonNull).distinct().toList();
|
||||
if (orderIds.isEmpty()) return measuredWeight;
|
||||
BigDecimal orderWeight = tailOrderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).in(LastlegOrderDetail::getOrderId, orderIds))
|
||||
.stream().map(LastlegOrderDetail::getGrossWeightKg).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return measuredWeight.add(orderWeight);
|
||||
}
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static Long parseLong(String value) {
|
||||
try { return StringUtils.hasText(value) ? Long.parseLong(value.trim()) : null; }
|
||||
|
||||
+11
@@ -38,6 +38,17 @@ public class DispatchController {
|
||||
return ApiResponse.success(service.update(dispatchNo, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{dispatchNo}/items/{itemId}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_EDIT + "')")
|
||||
@Operation(summary = "Remove one unpicked pallet before an outbound order is generated")
|
||||
public ApiResponse<DispatchResponse> removeItem(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@PathVariable Long itemId) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.removeItem(dispatchNo, itemId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{dispatchNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_CANCEL + "')")
|
||||
@Operation(summary = "Cancel a pending dispatch and release its pallet locks")
|
||||
|
||||
+3
-1
@@ -74,7 +74,9 @@ public class SmartDispatchController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody DispatchRequests.PreArrange request) {
|
||||
requirePlatform(user);
|
||||
String scopedCustomer = service.resolveCustomerScopeForPallets(request.planPalletIds());
|
||||
String scopedCustomer = request.relabelNo() == null || request.relabelNo().isBlank()
|
||||
? service.resolveCustomerScopeForPallets(request.planPalletIds())
|
||||
: service.resolveCustomerScopeForRelabel(request.relabelNo());
|
||||
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
return ApiResponse.success(service.preArrange(scopedCustomer, operatorId, request));
|
||||
}
|
||||
|
||||
+48
-7
@@ -53,7 +53,18 @@ public interface DispatchCandidateMapper {
|
||||
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage," +
|
||||
"(SELECT GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') " +
|
||||
"FROM ll_inbound_plan_carton c WHERE c.plan_pallet_id=p.id) AS cartonMarks " +
|
||||
"FROM ll_inbound_plan_carton c WHERE c.plan_pallet_id=p.id AND NOT EXISTS (" +
|
||||
"SELECT 1 FROM ll_relabel_item ri JOIN ll_inventory_position rp ON rp.id=ri.source_inventory_position_id " +
|
||||
"JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id " +
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.carton_mark=c.carton_mark AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED'))) AS cartonMarks," +
|
||||
"(SELECT GROUP_CONCAT(DISTINCT ri.fba_carton_mark ORDER BY ri.fba_carton_mark SEPARATOR ',') " +
|
||||
"FROM ll_relabel_item ri JOIN ll_inventory_position rp ON rp.id=ri.source_inventory_position_id " +
|
||||
"JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id " +
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
@@ -81,6 +92,9 @@ public interface DispatchCandidateMapper {
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE rli.customer_id=ip.customer_id AND rli.source_inbound_order_id IS NOT NULL " +
|
||||
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
|
||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||
"ORDER BY p.plan_id,p.sequence_no" +
|
||||
@@ -121,7 +135,18 @@ public interface DispatchCandidateMapper {
|
||||
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage," +
|
||||
"(SELECT GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') " +
|
||||
"FROM ll_inbound_plan_carton c WHERE c.plan_pallet_id=p.id) AS cartonMarks " +
|
||||
"FROM ll_inbound_plan_carton c WHERE c.plan_pallet_id=p.id AND NOT EXISTS (" +
|
||||
"SELECT 1 FROM ll_relabel_item ri JOIN ll_inventory_position rp ON rp.id=ri.source_inventory_position_id " +
|
||||
"JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id " +
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.carton_mark=c.carton_mark AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED'))) AS cartonMarks," +
|
||||
"(SELECT GROUP_CONCAT(DISTINCT ri.fba_carton_mark ORDER BY ri.fba_carton_mark SEPARATOR ',') " +
|
||||
"FROM ll_relabel_item ri JOIN ll_inventory_position rp ON rp.id=ri.source_inventory_position_id " +
|
||||
"JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id " +
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
@@ -137,6 +162,11 @@ public interface DispatchCandidateMapper {
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"JOIN ll_inbound_plan_line rpl ON rpl.order_id=rli.source_inbound_order_id " +
|
||||
"WHERE rli.customer_id=#{customerId} AND rli.source_inbound_order_id IS NOT NULL AND rpl.id IN " +
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id) " +
|
||||
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
|
||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||
"ORDER BY p.id FOR UPDATE" +
|
||||
@@ -148,20 +178,27 @@ public interface DispatchCandidateMapper {
|
||||
"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," +
|
||||
"MIN(COALESCE(NULLIF(ri.source_inbound_order_no,''),NULLIF(a.customer_order_no,''),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 " +
|
||||
"SUM(ri.notified_package_count) AS packageCount,MAX(wtp.volume_cbm) AS volumeCbm,'RELABEL' AS sourceStage," +
|
||||
"NULL AS cartonMarks,GROUP_CONCAT(DISTINCT COALESCE(NULLIF(ri.fba_carton_mark,''),ri.carton_mark) " +
|
||||
"ORDER BY COALESCE(NULLIF(ri.fba_carton_mark,''),ri.carton_mark) SEPARATOR ',') AS relabelCartonMarks " +
|
||||
"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 " +
|
||||
"LEFT JOIN ll_warehouse_transfer_order wto ON wto.relabel_no=a.relabel_no AND wto.customer_id=a.customer_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_pallet wtp ON wtp.transfer_order_id=wto.id AND wtp.customer_id=a.customer_id " +
|
||||
"AND wtp.warehouse_pallet_id=pos.pallet_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) " +
|
||||
"AND pos.hold_packages > 0 " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di JOIN ll_dispatch_order existing_dispatch " +
|
||||
"ON existing_dispatch.id=di.dispatch_order_id AND existing_dispatch.customer_id=di.customer_id " +
|
||||
"WHERE di.customer_id=a.customer_id AND di.source_stage='RELABEL' AND di.order_refs=a.relabel_no " +
|
||||
"AND existing_dispatch.status!='CANCELLED') " +
|
||||
"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,
|
||||
@@ -171,4 +208,8 @@ public interface DispatchCandidateMapper {
|
||||
@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);
|
||||
|
||||
@org.apache.ibatis.annotations.Update("UPDATE ll_relabel_application SET fulfillment_status='WAIT_PRE_DISPATCH',updated_at=CURRENT_TIMESTAMP(3) " +
|
||||
"WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND fulfillment_status='PRE_DISPATCH_CREATED'")
|
||||
int restoreRelabelPreDispatch(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
}
|
||||
|
||||
+7
-1
@@ -10,7 +10,13 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchDraftMapper extends BaseMapper<DispatchDraft> {
|
||||
@Select("SELECT * FROM ll_dispatch_draft WHERE operator_id=#{operatorId} AND status='ACTIVE' AND (expires_at IS NULL OR expires_at>CURRENT_TIMESTAMP(3)) ORDER BY updated_at DESC")
|
||||
@Select("SELECT d.* FROM ll_dispatch_draft d WHERE d.operator_id=#{operatorId} AND d.status='ACTIVE' " +
|
||||
"AND (d.expires_at IS NULL OR d.expires_at>CURRENT_TIMESTAMP(3)) AND NOT EXISTS (" +
|
||||
"SELECT 1 FROM ll_dispatch_pallet_lock l JOIN ll_outbound_order o ON o.customer_id=l.customer_id " +
|
||||
"AND o.dispatch_order_id=l.dispatch_order_id WHERE l.draft_token=d.draft_token) AND NOT EXISTS (" +
|
||||
"SELECT 1 FROM ll_dispatch_pallet_lock l JOIN ll_dispatch_order x ON x.id=l.dispatch_order_id " +
|
||||
"AND x.customer_id=l.customer_id WHERE l.draft_token=d.draft_token AND x.status!='PENDING_DEPARTURE') " +
|
||||
"ORDER BY d.updated_at DESC")
|
||||
List<DispatchDraft> findActiveByOperator(@Param("operatorId") String operatorId);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_draft WHERE draft_token=#{draftToken} AND operator_id=#{operatorId} AND status='ACTIVE' LIMIT 1 FOR UPDATE")
|
||||
|
||||
+12
@@ -15,6 +15,18 @@ public interface DispatchItemMapper extends BaseMapper<DispatchItem> {
|
||||
List<DispatchItem> findByDispatchOrder(@Param("customerId") String customerId,
|
||||
@Param("dispatchOrderId") Long dispatchOrderId);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_item WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} " +
|
||||
"AND id=#{itemId} LIMIT 1 FOR UPDATE")
|
||||
DispatchItem lockById(@Param("customerId") String customerId,
|
||||
@Param("dispatchOrderId") Long dispatchOrderId,
|
||||
@Param("itemId") Long itemId);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_item WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} " +
|
||||
"AND plan_pallet_id=#{planPalletId} LIMIT 1 FOR UPDATE")
|
||||
DispatchItem lockByPlanPallet(@Param("customerId") String customerId,
|
||||
@Param("dispatchOrderId") Long dispatchOrderId,
|
||||
@Param("planPalletId") Long planPalletId);
|
||||
|
||||
@Update("UPDATE ll_dispatch_item i JOIN ll_inbound_plan_pallet p ON p.id=i.plan_pallet_id " +
|
||||
"JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=i.customer_id " +
|
||||
"SET i.warehouse_pallet_id=p.warehouse_pallet_id,i.source_stage='RECEIVED' " +
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock>
|
||||
List<DispatchPalletLock> findActiveByDraftContainer(@Param("draftToken") String draftToken,
|
||||
@Param("draftContainerId") Long draftContainerId);
|
||||
|
||||
@Select("SELECT l.dispatch_order_id FROM ll_dispatch_pallet_lock l JOIN ll_dispatch_order d " +
|
||||
"ON d.id=l.dispatch_order_id AND d.customer_id=l.customer_id AND d.status='PENDING_DEPARTURE' " +
|
||||
"WHERE l.draft_token=#{draftToken} ORDER BY l.updated_at DESC,l.id DESC LIMIT 1")
|
||||
Long findDispatchOrderIdByDraft(@Param("draftToken") String draftToken);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_pallet_lock WHERE customer_id=#{customerId} AND source_pallet_key=#{sourcePalletKey} " +
|
||||
"AND status='ACTIVE' LIMIT 1 FOR UPDATE")
|
||||
DispatchPalletLock lockActive(@Param("customerId") String customerId,
|
||||
|
||||
+4
-3
@@ -29,7 +29,8 @@ public record DispatchPalletCandidate(
|
||||
Integer packageCount,
|
||||
BigDecimal volumeCbm,
|
||||
String sourceStage,
|
||||
String cartonMarks
|
||||
String cartonMarks,
|
||||
String relabelCartonMarks
|
||||
) {
|
||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||
String overseasWarehouseId, String overseasWarehouseName, String orderNo,
|
||||
@@ -40,7 +41,7 @@ public record DispatchPalletCandidate(
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, overseasWarehouseId,
|
||||
overseasWarehouseName, orderNo, orderRefs, inboundCode, palletNo, destinationWarehouseId,
|
||||
destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode, packageCount, volumeCbm,
|
||||
sourceStage, null);
|
||||
sourceStage, null, null);
|
||||
}
|
||||
|
||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||
@@ -50,7 +51,7 @@ public record DispatchPalletCandidate(
|
||||
BigDecimal volumeCbm, String sourceStage) {
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, null, null, orderNo, orderRefs, inboundCode,
|
||||
palletNo, destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
|
||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null);
|
||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null);
|
||||
}
|
||||
public String sourcePalletKey() {
|
||||
// A plan pallet keeps the same lock identity before and after the physical pallet is received.
|
||||
|
||||
+4
-2
@@ -11,8 +11,10 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchPdaPickOrderMapper extends BaseMapper<DispatchPdaPickOrder> {
|
||||
@Select("SELECT r.* FROM ll_inbound_stock_receipt r " +
|
||||
"WHERE r.customer_id=#{customerId} AND r.inventory_position_id=#{positionId} " +
|
||||
@Select("SELECT DISTINCT r.* FROM ll_inventory_position current_position " +
|
||||
"JOIN ll_inventory_position receipt_position ON receipt_position.balance_id=current_position.balance_id " +
|
||||
"JOIN ll_inbound_stock_receipt r ON r.inventory_position_id=receipt_position.id " +
|
||||
"WHERE current_position.id=#{positionId} AND r.customer_id=#{customerId} " +
|
||||
"AND r.received_packages > (SELECT COALESCE(SUM(a.packages),0) FROM ll_dispatch_pda_pick_order a " +
|
||||
"WHERE a.customer_id=r.customer_id AND a.receipt_id=r.id) ORDER BY r.received_at,r.id")
|
||||
List<InboundStockReceipt> availableReceipts(@Param("customerId") String customerId,
|
||||
|
||||
+26
-2
@@ -15,8 +15,11 @@ import com.baiyue.lastleg.dispatch.pda.entity.*;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.*;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -41,6 +44,8 @@ public class DispatchPdaWorkflowService {
|
||||
@Resource private DispatchPalletLockMapper palletLocks;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private RelabelApplicationService relabelApplications;
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Resource private DispatchPickOrderAttributionService pickAttribution;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@@ -111,6 +116,11 @@ public class DispatchPdaWorkflowService {
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":ALLOCATE", request.remark());
|
||||
if ("RELABEL_ORDER".equals(item.getSourceBusinessType())) {
|
||||
inventory.release(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), item.getSourceBusinessNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":RELABEL_RELEASE", "换标排车拣货前释放冻结库存"));
|
||||
}
|
||||
inventory.allocate(customerId, operatorId, move);
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
@@ -125,7 +135,8 @@ public class DispatchPdaWorkflowService {
|
||||
pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages());
|
||||
pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey());
|
||||
pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick);
|
||||
pickAttribution.attribute(customerId, pick);
|
||||
pickAttribution.attribute(customerId, pick,
|
||||
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null);
|
||||
long total = picked + request.packages();
|
||||
order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages()));
|
||||
task.setStatus("PICKING"); task.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -216,11 +227,22 @@ public class DispatchPdaWorkflowService {
|
||||
long shipped = dispatchPicks.stream().mapToLong(p -> n(p.getPackages())).sum();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
pickAttribution.depart(customerId, outbound.getOutboundNo(), order.getDispatchNo(), dispatchPicks, now);
|
||||
for (DispatchItem item : dispatchItems) {
|
||||
if (item.getOrderId() != null) warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
|
||||
if ("RELABEL_ORDER".equals(item.getSourceBusinessType()) && item.getSourceBusinessNo() != null) {
|
||||
warehouseTransfers.markOutboundByRelabel(customerId, item.getSourceBusinessNo(), now);
|
||||
}
|
||||
}
|
||||
dispatchItems.stream().filter(item -> "RELABEL_ORDER".equals(item.getSourceBusinessType()))
|
||||
.map(DispatchItem::getSourceBusinessNo).filter(Objects::nonNull).distinct().forEach(relabelNo ->
|
||||
relabelApplications.changeStatus(customerId, relabelNo, operatorId,
|
||||
new RelabelRequests.ChangeStatus("COMPLETED", "换标排车PDA发车完成")));
|
||||
outbound.setStatus("OUTBOUNDED");
|
||||
outbound.setUpdatedAt(now);
|
||||
outboundOrders.updateById(outbound);
|
||||
order.setShippedPackageCount((int) shipped);
|
||||
order.setStatus("DEPARTED");
|
||||
order.setDepartAt(now);
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
releasePalletLocks(customerId, order.getId(), now);
|
||||
@@ -235,11 +257,13 @@ public class DispatchPdaWorkflowService {
|
||||
for (DispatchItem item : source) {
|
||||
List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of()
|
||||
: positions.listPallet(customerId, item.getWarehousePalletId());
|
||||
boolean relabel = "RELABEL_ORDER".equals(item.getSourceBusinessType());
|
||||
values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(),
|
||||
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
|
||||
warehouseRouteLabel(item.getFbaWarehouseCode(), item.getDestinationWarehouseCode()), item.getPackageCount(),
|
||||
picks.totalItem(customerId, task.getId(), item.getId()), palletPositions.stream().map(p ->
|
||||
new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()),
|
||||
new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages())
|
||||
+ (relabel ? n(p.getHoldPackages()) : 0),
|
||||
n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList()));
|
||||
}
|
||||
OutboundOrder outbound = outboundOrders.findByDispatch(customerId, order.getId());
|
||||
|
||||
+13
@@ -27,6 +27,15 @@ public class DispatchPickOrderAttributionService {
|
||||
|
||||
/** Call after the inventory position has been locked/moved and the PDA pick row has been inserted. */
|
||||
public List<OrderPackages> attribute(String customerId, DispatchPdaPick pick) {
|
||||
return attribute(customerId, pick, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relabel inventory created before receipt-detail backfill may have no
|
||||
* ll_inbound_stock_receipt row. Its dispatch item still carries the
|
||||
* authoritative tail-leg order, so use that order only for relabel picks.
|
||||
*/
|
||||
public List<OrderPackages> attribute(String customerId, DispatchPdaPick pick, Long fallbackOrderId) {
|
||||
long remainingPackages = value(pick.getPackages());
|
||||
long totalPackages = remainingPackages;
|
||||
long totalQuantity = value(pick.getProductQuantity());
|
||||
@@ -58,6 +67,10 @@ public class DispatchPickOrderAttributionService {
|
||||
allocatedPackages = nextPackages;
|
||||
allocatedQuantity = nextQuantity;
|
||||
}
|
||||
if (remainingPackages != 0 && fallbackOrderId != null && fallbackOrderId > 0) {
|
||||
pickedByOrder.merge(fallbackOrderId, remainingPackages, Long::sum);
|
||||
remainingPackages = 0;
|
||||
}
|
||||
if (remainingPackages != 0) throw conflict("PDA pick cannot be attributed to inbound receipt orders");
|
||||
for (Map.Entry<Long, Long> entry : pickedByOrder.entrySet()) {
|
||||
if (orders.increasePicked(customerId, entry.getKey(), entry.getValue()) != 1) {
|
||||
|
||||
+133
-31
@@ -24,6 +24,7 @@ import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -36,7 +37,9 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -56,6 +59,7 @@ public class DispatchService {
|
||||
@Resource private DispatchPdaTaskMapper pdaTasks;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private LastlegOrderMapper tailOrders;
|
||||
@Resource private OutboundOrderMapper outboundOrders;
|
||||
|
||||
public String resolveCustomerScopeForPlans(List<Long> planIds) {
|
||||
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
|
||||
@@ -68,6 +72,12 @@ public class DispatchService {
|
||||
"Selected pallets");
|
||||
}
|
||||
|
||||
public String resolveCustomerScopeForRelabel(String relabelNo) {
|
||||
List<DispatchPalletCandidate> values = candidates.relabelAvailable(null, null, trimToNull(relabelNo));
|
||||
return requireSingleCustomerScope(values.stream().map(DispatchPalletCandidate::customerId)
|
||||
.filter(StringUtils::hasText).distinct().toList(), "Selected relabel order");
|
||||
}
|
||||
|
||||
public List<DispatchPalletCandidate> candidates(String customerId, List<Long> planIds, String orderNo,
|
||||
String fbaWarehouseCode, String sourceStage, String draftToken) {
|
||||
validateSourceStage(sourceStage);
|
||||
@@ -191,6 +201,15 @@ public class DispatchService {
|
||||
DispatchPalletLock active = activeLock(customerId, candidate);
|
||||
if (active == null) return;
|
||||
requireDraftOwner(active, operatorId, request.draftToken());
|
||||
if (active.getDispatchOrderId() != null) {
|
||||
DispatchOrder dispatch = orders.lockById(customerId, active.getDispatchOrderId());
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
requirePalletEditingAllowed(dispatch);
|
||||
DispatchItem item = items.lockByPlanPallet(customerId, dispatch.getId(), request.planPalletId());
|
||||
if (item != null) removeLockedItem(dispatch, item, List.of(active));
|
||||
else releaseLock(active, LocalDateTime.now());
|
||||
return;
|
||||
}
|
||||
releaseLock(active, LocalDateTime.now());
|
||||
}
|
||||
|
||||
@@ -199,7 +218,8 @@ public class DispatchService {
|
||||
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getDraftToken, draftToken)
|
||||
.eq(DispatchPalletLock::getOperatorId, trimToNull(operatorId))
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE")
|
||||
.isNull(DispatchPalletLock::getDispatchOrderId));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
activeLocks.forEach(lock -> releaseLock(lock, now));
|
||||
draftContainers.delete(new LambdaQueryWrapper<DispatchDraftContainer>().eq(DispatchDraftContainer::getDraftToken, draftToken));
|
||||
@@ -262,35 +282,60 @@ public class DispatchService {
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DispatchOrder dispatch = new DispatchOrder();
|
||||
dispatch.setId(IdWorker.getId());
|
||||
dispatch.setCustomerId(customerId);
|
||||
dispatch.setDispatchNo(orderNoGenerator.next("DISPATCH", selected.get(0).overseasWarehouseId(), dispatch.getId()));
|
||||
dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId());
|
||||
dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
|
||||
Long existingDispatchId = draftToken == null ? null : locks.findDispatchOrderIdByDraft(draftToken);
|
||||
DispatchOrder dispatch = existingDispatchId == null ? null : orders.lockById(customerId, existingDispatchId);
|
||||
boolean refreshing = dispatch != null;
|
||||
if (refreshing) {
|
||||
requirePalletEditingAllowed(dispatch);
|
||||
} else {
|
||||
dispatch = new DispatchOrder();
|
||||
dispatch.setId(IdWorker.getId());
|
||||
dispatch.setCustomerId(customerId);
|
||||
dispatch.setDispatchNo(orderNoGenerator.next("DISPATCH", selected.get(0).overseasWarehouseId(), dispatch.getId()));
|
||||
dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId());
|
||||
dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
|
||||
dispatch.setStatus(PENDING_DEPARTURE);
|
||||
dispatch.setPickedPackageCount(0);
|
||||
dispatch.setShippedPackageCount(0);
|
||||
dispatch.setCreatedBy(trimToNull(operatorId));
|
||||
dispatch.setCreatedAt(now);
|
||||
}
|
||||
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);
|
||||
if (refreshing) orders.updateById(dispatch); else orders.insert(dispatch);
|
||||
|
||||
List<DispatchItem> previousItems = items.findByDispatchOrder(customerId, dispatch.getId());
|
||||
Set<Long> selectedPalletIds = selected.stream().map(DispatchPalletCandidate::planPalletId)
|
||||
.collect(Collectors.toSet());
|
||||
if (refreshing) {
|
||||
List<DispatchPalletLock> previousLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getCustomerId, customerId)
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
previousItems.stream().filter(item -> item.getPlanPalletId() != null
|
||||
&& !selectedPalletIds.contains(item.getPlanPalletId()))
|
||||
.forEach(item -> {
|
||||
previousLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId()))
|
||||
.forEach(lock -> releaseLock(lock, now));
|
||||
items.deleteById(item.getId());
|
||||
});
|
||||
}
|
||||
Set<Long> existingPalletIds = previousItems.stream()
|
||||
.filter(item -> item.getPlanPalletId() == null || selectedPalletIds.contains(item.getPlanPalletId()))
|
||||
.map(DispatchItem::getPlanPalletId).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
|
||||
for (DispatchPalletCandidate candidate : selected) {
|
||||
DispatchPalletLock lock = activeLock(customerId, candidate);
|
||||
if (draftToken != null) {
|
||||
requireDraftOwner(lock, operatorId, draftToken);
|
||||
lock.setDispatchOrderId(dispatch.getId());
|
||||
lock.setDraftContainerId(null);
|
||||
lock.setDraftToken(null);
|
||||
lock.setExpiresAt(null);
|
||||
lock.setUpdatedAt(now);
|
||||
locks.updateById(lock);
|
||||
@@ -316,6 +361,7 @@ public class DispatchService {
|
||||
"Pallet is already locked by another active dispatch: " + candidate.palletNo(), exception);
|
||||
}
|
||||
}
|
||||
if (existingPalletIds.contains(candidate.planPalletId())) continue;
|
||||
DispatchItem item = new DispatchItem();
|
||||
item.setId(IdWorker.getId());
|
||||
item.setDispatchOrderId(dispatch.getId());
|
||||
@@ -337,27 +383,27 @@ public class DispatchService {
|
||||
item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0));
|
||||
item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO));
|
||||
item.setSourceStage(candidate.sourceStage());
|
||||
item.setCartonMarks(mergeCartonMarks(candidate.cartonMarks(), candidate.relabelCartonMarks()));
|
||||
item.setCreatedAt(now);
|
||||
items.insert(item);
|
||||
}
|
||||
DispatchPdaTask pdaTask = new DispatchPdaTask();
|
||||
pdaTask.setId(IdWorker.getId());
|
||||
pdaTask.setCustomerId(customerId);
|
||||
pdaTask.setDispatchOrderId(dispatch.getId());
|
||||
pdaTask.setTaskNo("DPDA" + pdaTask.getId());
|
||||
pdaTask.setStatus("PDA_PENDING");
|
||||
pdaTask.setWarehouseId(dispatch.getOverseasWarehouseId());
|
||||
pdaTask.setPushedAt(now);
|
||||
pdaTask.setCreatedAt(now);
|
||||
pdaTask.setUpdatedAt(now);
|
||||
pdaTasks.insert(pdaTask);
|
||||
if (!refreshing) {
|
||||
DispatchPdaTask pdaTask = new DispatchPdaTask();
|
||||
pdaTask.setId(IdWorker.getId());
|
||||
pdaTask.setCustomerId(customerId);
|
||||
pdaTask.setDispatchOrderId(dispatch.getId());
|
||||
pdaTask.setTaskNo("DPDA" + pdaTask.getId());
|
||||
pdaTask.setStatus("PDA_PENDING");
|
||||
pdaTask.setWarehouseId(dispatch.getOverseasWarehouseId());
|
||||
pdaTask.setPushedAt(now);
|
||||
pdaTask.setCreatedAt(now);
|
||||
pdaTask.setUpdatedAt(now);
|
||||
pdaTasks.insert(pdaTask);
|
||||
}
|
||||
if (draftToken != null) {
|
||||
draftContainers.delete(new LambdaQueryWrapper<DispatchDraftContainer>()
|
||||
.eq(DispatchDraftContainer::getDraftToken, draftToken));
|
||||
DispatchDraft draft = drafts.lockActive(draftToken, trimToNull(operatorId));
|
||||
if (draft != null) {
|
||||
draft.setStatus("RELEASED");
|
||||
draft.setReleasedAt(now);
|
||||
draft.setExpiresAt(null);
|
||||
draft.setUpdatedAt(now);
|
||||
drafts.updateById(draft);
|
||||
}
|
||||
@@ -377,7 +423,7 @@ public class DispatchService {
|
||||
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); }
|
||||
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(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO)); item.setSourceStage("RELABEL"); item.setCartonMarks(candidate.relabelCartonMarks()); 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()));
|
||||
@@ -463,6 +509,50 @@ public class DispatchService {
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchResponse removeItem(String dispatchNo, Long itemId) {
|
||||
DispatchOrder dispatch = orders.lockGlobalByDispatchNo(trimToNull(dispatchNo));
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
requirePalletEditingAllowed(dispatch);
|
||||
DispatchItem item = items.lockById(dispatch.getCustomerId(), dispatch.getId(), itemId);
|
||||
if (item == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch pallet not found");
|
||||
|
||||
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getCustomerId, dispatch.getCustomerId())
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
List<DispatchPalletLock> matchingLocks = activeLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId())
|
||||
|| (item.getWarehousePalletId() != null && Objects.equals(lock.getWarehousePalletId(), item.getWarehousePalletId())))
|
||||
.toList();
|
||||
return removeLockedItem(dispatch, item, matchingLocks);
|
||||
}
|
||||
|
||||
private DispatchResponse removeLockedItem(DispatchOrder dispatch, DispatchItem item,
|
||||
List<DispatchPalletLock> matchingLocks) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
matchingLocks.forEach(lock -> releaseLock(lock, now));
|
||||
items.deleteById(item.getId());
|
||||
|
||||
List<DispatchItem> remaining = items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId());
|
||||
dispatch.setItemCount(remaining.size());
|
||||
dispatch.setPalletCount(remaining.size());
|
||||
dispatch.setPackageCount(remaining.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull)
|
||||
.mapToInt(Integer::intValue).sum());
|
||||
dispatch.setUpdatedAt(now);
|
||||
orders.updateById(dispatch);
|
||||
return response(dispatch, remaining);
|
||||
}
|
||||
|
||||
private void requirePalletEditingAllowed(DispatchOrder dispatch) {
|
||||
if (!PENDING_DEPARTURE.equals(dispatch.getStatus())
|
||||
|| Objects.requireNonNullElse(dispatch.getPickedPackageCount(), 0) > 0) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Only an unpicked pending dispatch can edit pallets");
|
||||
}
|
||||
if (outboundOrders.findByDispatch(dispatch.getCustomerId(), dispatch.getId()) != null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "已生成出库单,不能再修改排车托盘");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancel(String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.lockGlobalByDispatchNo(trimToNull(dispatchNo));
|
||||
@@ -476,6 +566,10 @@ public class DispatchService {
|
||||
dispatch.setStatus(CANCELLED);
|
||||
dispatch.setUpdatedAt(now);
|
||||
orders.updateById(dispatch);
|
||||
items.findByDispatchOrder(customerId, dispatch.getId()).stream()
|
||||
.filter(item -> "RELABEL_ORDER".equals(item.getSourceBusinessType()))
|
||||
.map(DispatchItem::getSourceBusinessNo).filter(StringUtils::hasText).distinct()
|
||||
.forEach(relabelNo -> candidates.restoreRelabelPreDispatch(customerId, relabelNo));
|
||||
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getCustomerId, customerId)
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
|
||||
@@ -567,6 +661,14 @@ public class DispatchService {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
private static String mergeCartonMarks(String regularMarks, String relabelMarks) {
|
||||
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||
for (String source : List.of(Objects.toString(regularMarks, ""), Objects.toString(relabelMarks, ""))) {
|
||||
Arrays.stream(source.split(",")).map(String::trim).filter(StringUtils::hasText).forEach(values::add);
|
||||
}
|
||||
return values.isEmpty() ? null : String.join(",", values);
|
||||
}
|
||||
|
||||
private static void validateSourceStage(String sourceStage) {
|
||||
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
||||
|
||||
@@ -29,7 +29,6 @@ public record LastlegOrderSummary(
|
||||
String transferWarehouseName,
|
||||
String fbaWarehouseCode,
|
||||
String containerNo,
|
||||
String containerStatus,
|
||||
Boolean abnormal,
|
||||
Boolean detained,
|
||||
Boolean heldChargeable,
|
||||
@@ -46,6 +45,8 @@ public record LastlegOrderSummary(
|
||||
LocalDateTime actualArrivalAt,
|
||||
LocalDateTime estimatedWarehouseAt,
|
||||
String inboundOrderNo,
|
||||
@Schema(description = "排车状态", allowableValues = {"SCHEDULED", "UNSCHEDULED"})
|
||||
String dispatchStatus,
|
||||
String relabelOrderNo,
|
||||
String relabelStatus,
|
||||
String palletMergeFlag,
|
||||
|
||||
@@ -17,6 +17,15 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
String WAREHOUSE_FILTER_SQL = "(LOWER(TRIM(overseas_warehouse_code)) LIKE CONCAT('%',LOWER({0}),'%') " +
|
||||
"OR LOWER(TRIM(overseas_warehouse_name)) LIKE CONCAT('%',LOWER({0}),'%'))";
|
||||
|
||||
@Select("<script>SELECT o.id AS orderId,CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di " +
|
||||
"JOIN ll_dispatch_order d ON d.id=di.dispatch_order_id AND d.customer_id=di.customer_id " +
|
||||
"WHERE di.customer_id=o.customer_id AND di.order_id=o.id AND d.status!='CANCELLED') " +
|
||||
"THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END AS dispatchStatus FROM ll_order o WHERE o.id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
|
||||
List<DispatchStatusRow> findDispatchStatuses(@Param("orderIds") List<Long> orderIds);
|
||||
|
||||
record DispatchStatusRow(Long orderId, String dispatchStatus) { }
|
||||
|
||||
String CONTAINER_PROGRESS_FILTER = " FROM ll_order o LEFT JOIN ll_inbound_plan p " +
|
||||
"ON p.customer_id=o.customer_id AND p.source_order_id=o.id " +
|
||||
"WHERE o.source_container_booking_id IS NOT NULL AND o.source_container_booking_id<>'' " +
|
||||
|
||||
+7
@@ -28,6 +28,13 @@ public class PdaPalletOperationController {
|
||||
return ApiResponse.success(service.resolve(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/relabel-pallet-label")
|
||||
public ApiResponse<PalletOperationResponses.PrintablePallet> createRelabelPallet(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.NewRelabelPallet request) {
|
||||
return ApiResponse.success(service.createRelabelPallet(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/assemble")
|
||||
public ApiResponse<PalletOperationResponses.Operation> assemble(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+2
@@ -13,6 +13,8 @@ public final class PalletOperationRequests {
|
||||
private PalletOperationRequests() { }
|
||||
|
||||
public record Scan(@NotBlank @Size(max = 128) String code) { }
|
||||
public record NewRelabelPallet(@NotBlank @Size(max = 64) String relabelNo,
|
||||
@NotBlank @Size(max = 128) String idempotencyKey) { }
|
||||
public record PalletExpectation(@NotBlank @Size(max = 128) String palletCode,
|
||||
@PositiveOrZero int expectedVersion) { }
|
||||
public record MoveLine(@Positive long positionId, @Positive long packages,
|
||||
|
||||
+3
-1
@@ -11,8 +11,10 @@ public final class PalletOperationResponses {
|
||||
long availablePackages, long holdPackages, long allocatedPackages,
|
||||
long pickedPackages, long receivingPackages, long positionVersion) { }
|
||||
public record PalletContent(long palletId, String palletCode, String status, int palletVersion,
|
||||
long locationId, String locationCode, String destinationWarehouseCode,
|
||||
Long locationId, String locationCode, String destinationWarehouseCode,
|
||||
long totalPackages, List<InventoryLine> inventory) { }
|
||||
public record PrintablePallet(long palletId, String palletCode, int palletVersion,
|
||||
String relabelNo, String printableHtml) { }
|
||||
public record Operation(long operationId, String operationType, String idempotencyKey,
|
||||
String status, boolean replayed, LocalDateTime completedAt,
|
||||
List<PalletContent> pallets) {
|
||||
|
||||
+18
-2
@@ -28,6 +28,21 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
|
||||
restClientProperties.getPalletPhysical());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PalletReference ensureRelabelPallet(long warehouseId, String businessId) {
|
||||
Map<String, Object> body = Map.of("warehouseId", warehouseId, "businessId", businessId);
|
||||
ResolveEnvelope response = client.post().uri("/internal/v1/warehouse-pallets/ensure-relabel")
|
||||
.header("X-Internal-Service-Token", internalToken).body(body).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw palletLookupFailure(result.getStatusCode());
|
||||
}).body(ResolveEnvelope.class);
|
||||
if (response == null || response.data() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Warehouse pallet service returned no usable response");
|
||||
}
|
||||
return response.data();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PalletReference resolve(String palletCode) {
|
||||
ResolveEnvelope response = client.get().uri("/internal/v1/warehouse-pallets/resolve/{code}", palletCode)
|
||||
@@ -76,11 +91,12 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transition apply(long warehouseId, PalletReference pallet, String targetStatus, long targetLocationId,
|
||||
public Transition apply(long warehouseId, PalletReference pallet, String targetStatus, Long targetLocationId,
|
||||
String operationKey, String reason) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("warehouseId", warehouseId); body.put("expectedVersion", pallet.version());
|
||||
body.put("targetStatus", targetStatus); body.put("targetLocationId", targetLocationId);
|
||||
body.put("targetStatus", targetStatus);
|
||||
if (targetLocationId != null) body.put("targetLocationId", targetLocationId);
|
||||
body.put("operationKey", operationKey); if (reason != null) body.put("reason", reason);
|
||||
TransitionEnvelope response = client.post().uri("/internal/v1/pda-pallet-transitions/{id}", pallet.palletId())
|
||||
.header("X-Internal-Service-Token", internalToken).body(body).retrieve()
|
||||
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
package com.baiyue.lastleg.palletops.gateway;
|
||||
|
||||
public interface PalletPhysicalGateway {
|
||||
PalletReference ensureRelabelPallet(long warehouseId, String businessId);
|
||||
PalletReference resolve(String palletCode);
|
||||
PalletReference resolve(long palletId);
|
||||
LocationReference resolveLocation(long warehouseId, long locationId);
|
||||
Transition apply(long warehouseId, PalletReference pallet, String targetStatus, long targetLocationId,
|
||||
Transition apply(long warehouseId, PalletReference pallet, String targetStatus, Long targetLocationId,
|
||||
String operationKey, String reason);
|
||||
void compensate(long warehouseId, Transition transition);
|
||||
|
||||
record PalletReference(long palletId, String palletCode, long warehouseId, long locationId,
|
||||
record PalletReference(long palletId, String palletCode, long warehouseId, Long locationId,
|
||||
String status, String fbaWarehouseCode, java.math.BigDecimal unitPrice, int version) {
|
||||
public PalletReference(long palletId, String palletCode, long warehouseId, long locationId,
|
||||
public PalletReference(long palletId, String palletCode, long warehouseId, Long locationId,
|
||||
String status, String fbaWarehouseCode, int version) {
|
||||
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
|
||||
java.math.BigDecimal.ZERO, version);
|
||||
@@ -18,6 +19,6 @@ public interface PalletPhysicalGateway {
|
||||
}
|
||||
record LocationReference(long locationId, String locationCode, String locationType, String status) { }
|
||||
record Transition(String operationKey, long palletId, String palletCode, long warehouseId,
|
||||
String beforeStatus, String afterStatus, long beforeLocationId, long afterLocationId,
|
||||
String beforeStatus, String afterStatus, Long beforeLocationId, Long afterLocationId,
|
||||
int beforeVersion, int afterVersion, String operationStatus, boolean replayed) { }
|
||||
}
|
||||
|
||||
+33
-5
@@ -30,6 +30,7 @@ import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.DefaultTransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -77,6 +78,30 @@ public class PdaPalletOperationService {
|
||||
return content(identity.overseasWarehouseId(), pallet);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.PrintablePallet createRelabelPallet(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.NewRelabelPallet request) {
|
||||
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 冻结的海外仓");
|
||||
}
|
||||
String key = required(request.idempotencyKey(), "幂等键不能为空");
|
||||
PalletPhysicalGateway.PalletReference pallet = physical.ensureRelabelPallet(identity.overseasWarehouseId(),
|
||||
relabelNo + ":" + key);
|
||||
String code = HtmlUtils.htmlEscape(pallet.palletCode());
|
||||
String order = HtmlUtils.htmlEscape(relabelNo);
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code
|
||||
+ "</title><style>@page{size:100mm 70mm;margin:4mm}body{font-family:Arial,sans-serif;text-align:center;margin:0}"
|
||||
+ "h1{font-size:24px;margin:8mm 0 4mm;letter-spacing:0}.code{font-size:18px;font-weight:700;word-break:break-all}"
|
||||
+ ".ref{font-size:12px;margin-top:6mm}</style></head><body><h1>托盘标签</h1><div class=\"code\">"
|
||||
+ code + "</div><div class=\"ref\">换标单:" + order + "</div></body></html>";
|
||||
return new PalletOperationResponses.PrintablePallet(pallet.palletId(), pallet.palletCode(), pallet.version(),
|
||||
relabelNo, html);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation assemble(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Assemble request) {
|
||||
PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_ASSEMBLE",
|
||||
@@ -175,8 +200,10 @@ public class PdaPalletOperationService {
|
||||
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));
|
||||
String reason = "换标拆托" + (request.reason() == null || request.reason().isBlank()
|
||||
? "" : ":" + request.reason().trim());
|
||||
return execute(identity, "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(), request,
|
||||
request.reason(), relabelPlan);
|
||||
reason, relabelPlan);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity,
|
||||
@@ -563,9 +590,10 @@ public class PdaPalletOperationService {
|
||||
.digest((operator + "|" + key + "|" + palletId).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
|
||||
}
|
||||
private String locationCode(long warehouseId, long locationId) {
|
||||
private String locationCode(long warehouseId, Long locationId) {
|
||||
if (locationId == null) return null;
|
||||
PalletPhysicalGateway.LocationReference location = physical.resolveLocation(warehouseId, locationId);
|
||||
if (location == null || location.locationId() != locationId || "INACTIVE".equals(location.status())) {
|
||||
if (location == null || location.locationId() != locationId.longValue() || "INACTIVE".equals(location.status())) {
|
||||
throw conflict("托盘库位不存在或已停用");
|
||||
}
|
||||
return location.locationCode();
|
||||
@@ -622,10 +650,10 @@ public class PdaPalletOperationService {
|
||||
|
||||
private record PlannedMove(long positionId, long packages, long expectedVersion,
|
||||
long sourcePalletId, String sourcePalletCode,
|
||||
long targetPalletId, String targetPalletCode, long targetLocationId,
|
||||
long targetPalletId, String targetPalletCode, Long targetLocationId,
|
||||
String targetLocationCode, InventoryBucket bucket) { }
|
||||
private record PalletChange(PalletPhysicalGateway.PalletReference pallet, String targetStatus,
|
||||
long targetLocationId) { }
|
||||
Long targetLocationId) { }
|
||||
private record Plan(Map<String, PalletPhysicalGateway.PalletReference> pallets,
|
||||
List<PlannedMove> moves, List<PalletChange> changes,
|
||||
RelabelMutation relabelMutation) { }
|
||||
|
||||
+7
@@ -30,6 +30,13 @@ public class MerchantRelabelApplicationController {
|
||||
@RequestParam(required = false) String inboundCode) {
|
||||
return ApiResponse.success(service.candidates(customer(user), operationWarehouseId, inboundCode));
|
||||
}
|
||||
@GetMapping("/historical-orders")
|
||||
public ApiResponse<List<RelabelResponses.HistoricalOrder>> historicalOrders(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String operationWarehouseId,
|
||||
@RequestParam(required = false, defaultValue = "") String keyword) {
|
||||
return ApiResponse.success(service.historicalOrders(customer(user), operationWarehouseId, keyword));
|
||||
}
|
||||
@PostMapping
|
||||
public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody RelabelRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||
|
||||
+20
@@ -61,6 +61,26 @@ public class RelabelApplicationController {
|
||||
return ApiResponse.success(service.changeStatus(null, relabelNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{relabelNo}/wallet-freeze/retry")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_STATUS + "')")
|
||||
@Operation(summary = "Retry a failed relabel wallet freeze", description = "Required authority: " + OverseasPermissions.RELABEL_ORDER_STATUS)
|
||||
public ApiResponse<RelabelResponses.Detail> retryWalletFreeze(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo,
|
||||
@Valid @RequestBody RelabelRequests.WalletFreezeRetry request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.retryWalletFreeze(relabelNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{relabelNo}/wallet-settlement/retry")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_STATUS + "')")
|
||||
@Operation(summary = "Retry a failed relabel wallet settlement", description = "Required authority: " + OverseasPermissions.RELABEL_ORDER_STATUS)
|
||||
public ApiResponse<RelabelResponses.Detail> retryWalletSettlement(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo,
|
||||
@Valid @RequestBody RelabelRequests.WalletFreezeRetry request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.retryWalletSettlement(relabelNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{relabelNo}/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_CREATE + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> uploadFile(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -117,4 +117,7 @@ public final class RelabelRequests {
|
||||
|
||||
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record WalletFreezeRetry(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ public final class RelabelResponses {
|
||||
String overseasWarehouseName, String destinationFbaWarehouseCode,
|
||||
String destinationFbaWarehouseName, String skuCode, String productNameZh,
|
||||
long availablePackages, long version, List<InventoryPosition> positions) {}
|
||||
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
|
||||
String relabelOrderNo, String orderType, String status, Integer packageCount,
|
||||
LocalDateTime createdAt) {}
|
||||
public record TailOrder(Long id, String orderNo, String overseasWarehouseId, int packageCount,
|
||||
String status) {}
|
||||
public record History(String fromStatus, String toStatus, String operatorId,
|
||||
|
||||
@@ -43,4 +43,7 @@ public class RelabelItem {
|
||||
private String sourceWarehouseCode;
|
||||
private String sourceWarehouseName;
|
||||
private LocalDateTime createdAt;
|
||||
private Long returnCartonId;
|
||||
private String sourceCartonMark;
|
||||
private String newCartonMark;
|
||||
}
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication> {
|
||||
@@ -25,4 +26,11 @@ public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication>
|
||||
|
||||
@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);
|
||||
|
||||
@Update("UPDATE ll_relabel_application SET wallet_status=#{status}, wallet_error_code=#{errorCode}, "
|
||||
+ "wallet_error_message=#{errorMessage}, wallet_command_event_id=COALESCE(#{eventId},wallet_command_event_id), "
|
||||
+ "updated_at=NOW(3) WHERE id=#{id}")
|
||||
int updateWalletState(@Param("id") Long id, @Param("status") String status,
|
||||
@Param("errorCode") String errorCode, @Param("errorMessage") String errorMessage,
|
||||
@Param("eventId") String eventId);
|
||||
}
|
||||
|
||||
+18
@@ -18,6 +18,8 @@ import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelOperationMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelUnitFactMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
@@ -60,6 +62,7 @@ public class PdaRelabelService {
|
||||
@Resource private RelabelItemMapper items;
|
||||
@Resource private PdaRelabelTaskMapper tasks;
|
||||
@Resource private PdaRelabelUnitFactMapper unitFacts;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private PdaRelabelOperationMapper operations;
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private RelabelApplicationService applicationService;
|
||||
@@ -296,6 +299,21 @@ public class PdaRelabelService {
|
||||
fact.setOperatorKey(operatorKey); fact.setOperatorName(operatorName); fact.setOccurredAt(LocalDateTime.now());
|
||||
try { unitFacts.insert(fact); }
|
||||
catch (DuplicateKeyException duplicate) { throw conflict("Old label was processed concurrently"); }
|
||||
if ("RELABELLED".equals(resultCode)) {
|
||||
item.setNewCartonMark(resolvedNewLabel);
|
||||
item.setFbaCartonMark(resolvedNewLabel);
|
||||
items.updateById(item);
|
||||
if (item.getReturnCartonId() != null && returnCartons != null) {
|
||||
ReturnInventoryCarton carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
if (carton != null && application.getCustomerId().equals(carton.getCustomerId())) {
|
||||
carton.setNewCartonMark(resolvedNewLabel);
|
||||
carton.setFbaCartonMark(resolvedNewLabel);
|
||||
carton.setRelabelStatus(PROCESSING);
|
||||
carton.setUpdatedAt(LocalDateTime.now());
|
||||
returnCartons.updateById(carton);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("RELABELLED".equals(resultCode)) task.setRelabelledPackages(value(task.getRelabelledPackages()) + 1);
|
||||
if ("SKIPPED".equals(resultCode)) task.setSkippedPackages(value(task.getSkippedPackages()) + 1);
|
||||
if ("EXCEPTION".equals(resultCode)) task.setExceptionPackages(value(task.getExceptionPackages()) + 1);
|
||||
|
||||
+119
-3
@@ -38,6 +38,8 @@ 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;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import com.baiyue.common.event.WalletCommandCompletedV1;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -89,6 +91,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private OutboundPalletItemMapper outboundItems;
|
||||
@Resource private OutboundPdaTaskMapper outboundTasks;
|
||||
@Resource private OverseasFeeSourceService feeSources;
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String customerId(String relabelNo) { return require(null, relabelNo).getCustomerId(); }
|
||||
@@ -233,6 +236,32 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
balance.getProductNameZh(), available, value(balance.getVersion()), positions);
|
||||
}).filter(candidate -> candidate.availablePackages() > 0).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<RelabelResponses.HistoricalOrder> historicalOrders(String customerId, String operationWarehouseId,
|
||||
String keyword) {
|
||||
if (!StringUtils.hasText(operationWarehouseId)) throw badRequest("Operation warehouse is required");
|
||||
String q = StringUtils.hasText(keyword) ? keyword.trim() : "";
|
||||
List<LastlegOrder> orders = this.orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getOverseasWarehouseId, operationWarehouseId.trim())
|
||||
.and(w -> w.like(StringUtils.hasText(q), LastlegOrder::getOrderNo, q)
|
||||
.or().like(StringUtils.hasText(q), LastlegOrder::getSourceOrderNo, q)
|
||||
.or().like(StringUtils.hasText(q), LastlegOrder::getRelabelOrderNo, q)
|
||||
.or().like(StringUtils.hasText(q), LastlegOrder::getOutboundOrderNo, q))
|
||||
.orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId)
|
||||
.last("LIMIT 100"));
|
||||
Map<String, RelabelResponses.HistoricalOrder> result = new java.util.LinkedHashMap<>();
|
||||
orders.forEach(order -> result.put("ORDER:" + order.getId(), new RelabelResponses.HistoricalOrder(order.getId(),
|
||||
order.getOrderNo(), order.getSourceOrderNo(), order.getOutboundOrderNo(), order.getRelabelOrderNo(),
|
||||
order.getOrderType(), order.getCurrentStatus(), order.getPackageCount(), order.getCreatedAt())));
|
||||
for (OutboundOrder outbound : outboundOrders.listRelabelOutbounds(customerId, operationWarehouseId.trim(), q)) {
|
||||
result.putIfAbsent("OUTBOUND:" + outbound.getId(), new RelabelResponses.HistoricalOrder(null,
|
||||
outbound.getOutboundNo(), null, outbound.getOutboundNo(), null, outbound.getOutboundType(),
|
||||
outbound.getStatus(), outbound.getPackageCount(), outbound.getCreatedAt()));
|
||||
}
|
||||
return result.values().stream().limit(100).toList();
|
||||
}
|
||||
@Transactional
|
||||
public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
@@ -411,7 +440,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setFulfillmentStatus("WAIT_CUSTOMER_CONFIRMATION");
|
||||
applications.updateById(application);
|
||||
} else if ("COMPLETED".equals(next)) {
|
||||
if (!StringUtils.hasText(application.getOutboundOrderNo())) releaseInventory(customerId, operatorId, application);
|
||||
boolean preDispatched = "PRE_DISPATCH_CREATED".equals(application.getFulfillmentStatus());
|
||||
application.setFulfillmentStatus("OUTBOUNDED");
|
||||
if (!StringUtils.hasText(application.getOutboundOrderNo())
|
||||
&& !preDispatched) {
|
||||
releaseInventory(customerId, operatorId, application);
|
||||
}
|
||||
BigDecimal quantity = BigDecimal.valueOf(Objects.requireNonNullElse(application.getTotalPackageCount(), 0));
|
||||
feeSources.publishApproved(operatorId, "RELABEL:" + relabelNo,
|
||||
new OverseasFeeSourceDtos.SaveRequest(customerId, linkedOrders.get(0).getCustomerName(),
|
||||
@@ -457,6 +491,62 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail retryWalletFreeze(String relabelNo, String operatorId,
|
||||
RelabelRequests.WalletFreezeRetry request) {
|
||||
RelabelApplication application = applications.lockAnyByRelabelNo(relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
String customerId = application.getCustomerId();
|
||||
if ("FROZEN".equals(application.getWalletStatus()) || "FREEZE_PENDING".equals(application.getWalletStatus())) {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
if (!PENDING_PROCESSING.equals(application.getStatus()) || !"FREEZE_FAILED".equals(application.getWalletStatus())) {
|
||||
throw conflict("Only a pending relabel application with failed funds freeze can be retried");
|
||||
}
|
||||
String retryKey = request.idempotencyKey().trim();
|
||||
long walletVersion = 100L + Long.parseLong(hash(retryKey).substring(0, 15), 16);
|
||||
int holdIndex = 0;
|
||||
for (RelabelItem item : itemList(application.getId())) {
|
||||
if (item.getSourceInventoryPositionId() == null
|
||||
|| Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) <= 0) continue;
|
||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo,
|
||||
null, "RELABEL:" + relabelNo, "RELABEL:" + application.getId() + ":RETRY_HOLD:"
|
||||
+ walletVersion + ":" + holdIndex++, "补款后重试冻结换标库存"));
|
||||
}
|
||||
String walletEventId = walletCommands.publish("FREEZE", Long.parseLong(customerId), "RELABEL_ORDER",
|
||||
relabelNo, application.getChargeAmount(), trim(request.remark()), walletVersion);
|
||||
application.setWalletStatus("FREEZE_PENDING");
|
||||
application.setWalletErrorCode(null); application.setWalletErrorMessage(null);
|
||||
application.setWalletCommandEventId(walletEventId); application.setUpdatedAt(LocalDateTime.now());
|
||||
applications.updateWalletState(application.getId(), application.getWalletStatus(), null, null, walletEventId);
|
||||
insertHistory(customerId, application.getId(), application.getStatus(), application.getStatus(), operatorId,
|
||||
"Wallet freeze retry requested", LocalDateTime.now());
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail retryWalletSettlement(String relabelNo, String operatorId,
|
||||
RelabelRequests.WalletFreezeRetry request) {
|
||||
RelabelApplication application = applications.lockAnyByRelabelNo(relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
String customerId = application.getCustomerId();
|
||||
if ("SETTLED".equals(application.getWalletStatus()) || "SETTLE_PENDING".equals(application.getWalletStatus())) {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
if (!"COMPLETED".equals(application.getStatus()) || !"SETTLE_FAILED".equals(application.getWalletStatus())) {
|
||||
throw conflict("Only a completed relabel application with failed funds settlement can be retried");
|
||||
}
|
||||
String retryKey = request.idempotencyKey().trim();
|
||||
long walletVersion = 200L + Long.parseLong(hash(retryKey).substring(0, 15), 16);
|
||||
String walletEventId = walletCommands.publish("SETTLE", Long.parseLong(customerId), "RELABEL_ORDER",
|
||||
relabelNo, null, trim(request.remark()), walletVersion);
|
||||
applications.updateWalletState(application.getId(), "SETTLE_PENDING", null, null, walletEventId);
|
||||
insertHistory(customerId, application.getId(), application.getStatus(), application.getStatus(), operatorId,
|
||||
"Wallet settlement retry requested", LocalDateTime.now());
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail confirmCompletion(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.CustomerConfirm request) {
|
||||
@@ -492,12 +582,37 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
if (!Objects.equals(orderBefore, order.getCurrentStatus())) {
|
||||
insertOrderHistory(customerId, order.getId(), 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);
|
||||
// 仓储转运换标在客户确认后立即恢复为可排车,发车时再转为 OUTBOUNDED。
|
||||
if (warehouseTransfers != null) {
|
||||
WarehouseTransferOrder transfer = warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
if (transfer != null && "WAIT_RELABEL".equals(transfer.getStatus())) {
|
||||
transfer.setStatus("READY_FOR_DISPATCH");
|
||||
transfer.setRelabelCompletedAt(now);
|
||||
transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
|
||||
LastlegOrder transferOrder = orders.selectById(transfer.getLastlegOrderId());
|
||||
if (transferOrder != null && customerId.equals(transferOrder.getCustomerId())) {
|
||||
String transferOrderBefore = transferOrder.getCurrentStatus();
|
||||
transferOrder.setCurrentStatus("READY_FOR_DISPATCH");
|
||||
transferOrder.setRelabelStatus(READY_FOR_OUTBOUND);
|
||||
transferOrder.setSubOrderStatus("WAREHOUSE_TRANSFER:READY_FOR_DISPATCH;RELABEL:"
|
||||
+ READY_FOR_OUTBOUND);
|
||||
transferOrder.setUpdatedAt(now);
|
||||
orders.updateById(transferOrder);
|
||||
if (!Objects.equals(transferOrderBefore, transferOrder.getCurrentStatus())) {
|
||||
insertOrderHistory(customerId, transferOrder.getId(), transferOrderBefore,
|
||||
transferOrder.getCurrentStatus(), "Warehouse transfer relabel completed", now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@@ -622,7 +737,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
releaseInventory(value.getCustomerId(), "WALLET_CALLBACK", value);
|
||||
}
|
||||
}
|
||||
value.setUpdatedAt(LocalDateTime.now()); applications.updateById(value);
|
||||
applications.updateWalletState(value.getId(), value.getWalletStatus(), value.getWalletErrorCode(),
|
||||
value.getWalletErrorMessage(), null);
|
||||
}
|
||||
|
||||
private RelabelResponses.Item item(RelabelItem value) {
|
||||
|
||||
+9
@@ -23,6 +23,15 @@ public class MerchantReturnOrderController {
|
||||
@Valid ReturnOrderRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); }
|
||||
@GetMapping("/{returnNo}") public ApiResponse<ReturnOrderResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String returnNo) { return ApiResponse.success(service.detail(customer(user), returnNo)); }
|
||||
@GetMapping("/{returnNo}/relabel-candidates") public ApiResponse<java.util.List<ReturnOrderResponses.RelabelCandidate>> relabelCandidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo) {
|
||||
return ApiResponse.success(service.relabelCandidates(customer(user), returnNo));
|
||||
}
|
||||
@PostMapping("/{returnNo}/relabel") public ApiResponse<ReturnOrderResponses.RelabelCreated> createRelabel(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo,
|
||||
@Valid @RequestBody ReturnOrderRequests.RelabelCreate request) {
|
||||
return ApiResponse.success(service.createRelabel(customer(user), operator(user), returnNo, request));
|
||||
}
|
||||
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");
|
||||
|
||||
+4
@@ -27,4 +27,8 @@ public final class ReturnOrderRequests {
|
||||
String trackingNo, String skuCode, String status) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
public record RelabelCreate(@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> cartonMarks,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 128) String contactName, @Size(max = 64) String contactPhone,
|
||||
@Size(max = 32) String deliveryMethod, @Size(max = 1000) String remark) {}
|
||||
}
|
||||
|
||||
+4
@@ -14,4 +14,8 @@ public final class ReturnOrderResponses {
|
||||
public record Detail(Summary order, String remark, List<Item> items, List<Receipt> receipts) { }
|
||||
public record Receipt(String receiptNo, String receiptBatchNo, String cartonMark, String trackingNo, String startedBy,
|
||||
long receivedQuantity, String operatorName, LocalDateTime signedAt) { }
|
||||
public record RelabelCandidate(Long cartonId, String cartonMark, String sourceReturnNo, String status,
|
||||
long totalQuantity, List<Item> items, String relabelStatus) { }
|
||||
public record RelabelCreated(String relabelNo, Long relabelApplicationId, String returnNo,
|
||||
List<String> cartonMarks, String status) { }
|
||||
}
|
||||
|
||||
+1
@@ -36,4 +36,5 @@ public class ReturnInventoryCarton {
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime mergedAt;
|
||||
private String newCartonMark;
|
||||
}
|
||||
|
||||
+42
-1
@@ -18,6 +18,10 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryQueryMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryMovementMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.model.ReturnInventoryRow;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -52,6 +56,8 @@ public class ReturnInventoryService {
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper orders;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper receiptMapper;
|
||||
@Resource private LastlegOrderMapper lastlegOrders;
|
||||
@Resource private LastlegOrderDetailMapper lastlegOrderDetails;
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryCarton createReceiptCarton(ReturnOrder order, ReturnReceipt receipt,
|
||||
@@ -66,6 +72,7 @@ public class ReturnInventoryService {
|
||||
ReturnInventoryCarton carton = new ReturnInventoryCarton();
|
||||
carton.setId(cartonId); carton.setCustomerId(order.getCustomerId()); carton.setOverseasWarehouseId(order.getOverseasWarehouseId());
|
||||
carton.setSourceReceiptId(receipt.getId()); carton.setSourceBatchNo(receipt.getReceiptBatchNo()); carton.setCartonMark(cartonMark);
|
||||
carton.setSourceReturnNo(order.getReturnNo());
|
||||
carton.setStatus(AVAILABLE); carton.setSkuTypes(lines.size()); carton.setTotalQuantity(total); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
for (ReceiptLine line : lines) {
|
||||
@@ -216,9 +223,13 @@ public class ReturnInventoryService {
|
||||
if (selectedOrders.stream().anyMatch(order -> !"COMPLETED".equals(order.getStatus()))) throw conflict("只能合并已完成签收的退货单");
|
||||
if (selectedOrders.stream().anyMatch(order -> !customerId.equals(order.getCustomerId()))) throw conflict("不同客户的退货单禁止合箱");
|
||||
List<ReturnInventoryCarton> sources = new ArrayList<>();
|
||||
Map<Long, ReturnOrder> sourceOwners = new LinkedHashMap<>();
|
||||
for (ReturnOrder order : selectedOrders) for (ReturnReceipt receipt : receiptMapper.listOrder(order.getId())) {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
if (carton != null && AVAILABLE.equals(carton.getStatus())) sources.add(cartons.lockMark(carton.getCartonMark()));
|
||||
if (carton != null && AVAILABLE.equals(carton.getStatus())) {
|
||||
ReturnInventoryCarton locked = cartons.lockMark(carton.getCartonMark());
|
||||
sources.add(locked); sourceOwners.put(locked.getId(), order);
|
||||
}
|
||||
}
|
||||
if (sources.isEmpty()) throw conflict("所选退货单没有可合箱的库存箱");
|
||||
Map<String, Long> requested = new LinkedHashMap<>();
|
||||
@@ -229,6 +240,7 @@ public class ReturnInventoryService {
|
||||
}
|
||||
Map<String, Long> remaining = new LinkedHashMap<>(requested);
|
||||
Map<String, Integer> emptiedBySku = new LinkedHashMap<>();
|
||||
Map<Long, Map<String, Long>> attachedByOrder = new LinkedHashMap<>();
|
||||
long total = 0;
|
||||
for (ReturnInventoryCarton source : sources) {
|
||||
List<ReturnInventoryCartonItem> sourceItems = cartonItems.listCarton(source.getId());
|
||||
@@ -238,6 +250,9 @@ public class ReturnInventoryService {
|
||||
long take = Math.min(value(remaining.get(sku)), value(item.getQuantity()));
|
||||
if (take > 0) {
|
||||
remaining.put(sku, remaining.get(sku) - take); total = Math.addExact(total, take);
|
||||
ReturnOrder owner = sourceOwners.get(source.getId());
|
||||
if (owner != null) attachedByOrder.computeIfAbsent(owner.getId(), ignored -> new LinkedHashMap<>())
|
||||
.merge(sku, take, Math::addExact);
|
||||
productNames.putIfAbsent(sku, item.getProductNameZh());
|
||||
item.setQuantity(item.getQuantity() - take);
|
||||
if (item.getQuantity() == 0) cartonItems.deleteById(item.getId()); else cartonItems.updateById(item);
|
||||
@@ -267,10 +282,36 @@ public class ReturnInventoryService {
|
||||
balances.adjust(IdWorker.getId(), customerId, target.getOverseasWarehouseId(), entry.getKey(), productNames.get(entry.getKey()), 0,
|
||||
-entry.getValue(), cartonDelta);
|
||||
}
|
||||
attachMergedCarton(target, attachedByOrder, selectedOrders, productNames, now);
|
||||
operation.setResultCartonId(targetId); operation.setUpdatedAt(now); operations.updateById(operation);
|
||||
return new ReturnInventoryResponses.MergeResult(target.getCartonMark(), requested.keySet().iterator().next(), total, label(target));
|
||||
}
|
||||
|
||||
private void attachMergedCarton(ReturnInventoryCarton target, Map<Long, Map<String, Long>> attachedByOrder,
|
||||
List<ReturnOrder> selectedOrders, Map<String, String> productNames, LocalDateTime now) {
|
||||
Map<Long, ReturnOrder> byId = selectedOrders.stream().collect(java.util.stream.Collectors.toMap(ReturnOrder::getId, value -> value));
|
||||
for (Map.Entry<Long, Map<String, Long>> orderEntry : attachedByOrder.entrySet()) {
|
||||
ReturnOrder returnOrder = byId.get(orderEntry.getKey());
|
||||
if (returnOrder == null || returnOrder.getLastlegOrderId() == null) continue;
|
||||
LastlegOrder tail = lastlegOrders.selectById(returnOrder.getLastlegOrderId());
|
||||
if (tail == null || !returnOrder.getCustomerId().equals(tail.getCustomerId())) continue;
|
||||
long quantity = orderEntry.getValue().values().stream().mapToLong(Long::longValue).sum();
|
||||
boolean firstSku = true;
|
||||
for (Map.Entry<String, Long> skuEntry : orderEntry.getValue().entrySet()) {
|
||||
LastlegOrderDetail detail = new LastlegOrderDetail();
|
||||
detail.setId(IdWorker.getId()); detail.setCustomerId(returnOrder.getCustomerId()); detail.setOrderId(tail.getId());
|
||||
detail.setSourceInboundDetailId("RETURN_CARTON:" + target.getId() + ":" + returnOrder.getId() + ":" + skuEntry.getKey());
|
||||
detail.setSkuCode(skuEntry.getKey()); detail.setProductNameZh(productNames.get(skuEntry.getKey()));
|
||||
detail.setPackageCount(firstSku ? 1 : 0); detail.setProductQuantity(Math.toIntExact(skuEntry.getValue())); detail.setShippingMark(target.getCartonMark());
|
||||
detail.setDestinationWarehouseCode(target.getDestinationWarehouseCode()); detail.setDestinationWarehouseName(target.getDestinationWarehouseName());
|
||||
lastlegOrderDetails.insert(detail); firstSku = false;
|
||||
}
|
||||
tail.setPackageCount(Math.addExact(tail.getPackageCount() == null ? 0 : tail.getPackageCount(), 1));
|
||||
tail.setProductQuantity(Math.addExact(tail.getProductQuantity() == null ? 0 : tail.getProductQuantity(), Math.toIntExact(quantity)));
|
||||
tail.setUpdatedAt(now); lastlegOrders.updateById(tail);
|
||||
}
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.MergeResult replayMerge(ReturnInventoryOperation operation, String requestHash) {
|
||||
if (!requestHash.equals(operation.getRequestHash())) throw conflict("幂等键已用于不同的合箱请求");
|
||||
if (operation.getResultCartonId() == null) throw conflict("合箱请求正在处理");
|
||||
|
||||
+67
@@ -21,6 +21,14 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
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.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -59,6 +67,10 @@ public class ReturnReceiptService {
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private LastlegOrderMapper lastlegOrders;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper cartonItems;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail create(String customerId, String operatorId, ReturnOrderRequests.Create request) {
|
||||
@@ -159,6 +171,61 @@ public class ReturnReceiptService {
|
||||
return new ReturnOrderResponses.Detail(summary(order), order.getRemark(), items, receiptViews);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReturnOrderResponses.RelabelCandidate> relabelCandidates(String customerId, String returnNo) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货单不存在");
|
||||
if (!COMPLETED.equals(order.getStatus())) throw conflict("退货单尚未完成签收");
|
||||
List<ReturnOrderResponses.RelabelCandidate> result = new ArrayList<>();
|
||||
for (ReturnReceipt receipt : receipts.listOrder(order.getId())) {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
if (carton == null || !ReturnInventoryService.AVAILABLE.equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) continue;
|
||||
List<ReturnOrderResponses.Item> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(i -> new ReturnOrderResponses.Item(i.getSkuCode(), i.getProductNameZh(), value(i.getQuantity()), value(i.getQuantity()))).toList();
|
||||
result.add(new ReturnOrderResponses.RelabelCandidate(carton.getId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getStatus(), value(carton.getTotalQuantity()), items, carton.getRelabelStatus()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.RelabelCreated createRelabel(String customerId, String operatorId, String returnNo,
|
||||
ReturnOrderRequests.RelabelCreate request) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货单不存在");
|
||||
if (!COMPLETED.equals(order.getStatus())) throw conflict("退货单尚未完成签收");
|
||||
String key = text(request.idempotencyKey());
|
||||
RelabelApplication replay = relabelApplications.findKey(customerId, key);
|
||||
if (replay != null) return new ReturnOrderResponses.RelabelCreated(replay.getRelabelNo(), replay.getId(), order.getReturnNo(), request.cartonMarks(), replay.getStatus());
|
||||
List<ReturnInventoryCarton> selected = new ArrayList<>();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
for (String mark : request.cartonMarks()) {
|
||||
String normalized = text(mark);
|
||||
if (!seen.add(normalized.toUpperCase(java.util.Locale.ROOT))) throw badRequest("箱唛不能重复");
|
||||
ReturnInventoryCarton carton = cartons.findCustomerMark(customerId, normalized);
|
||||
if (carton == null || !order.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())) throw notFound("退货箱不存在: " + normalized);
|
||||
if (!ReturnInventoryService.AVAILABLE.equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) throw conflict("退货箱不可申请换标: " + normalized);
|
||||
if (carton.getSourceReturnNo() == null || !java.util.Arrays.asList(carton.getSourceReturnNo().split(",")).contains(order.getReturnNo())) throw conflict("退货箱不属于当前退货单: " + normalized);
|
||||
selected.add(cartons.lockMark(normalized));
|
||||
}
|
||||
long appId = IdWorker.getId(); LocalDateTime now = LocalDateTime.now();
|
||||
String relabelNo = numbers.next("RELABEL", order.getOverseasWarehouseId(), appId);
|
||||
LastlegOrder tail = new LastlegOrder(); tail.setId(IdWorker.getId()); tail.setOrderNo(numbers.next("RELABEL", order.getOverseasWarehouseId(), tail.getId()));
|
||||
tail.setCustomerId(customerId); tail.setOrderType("RELABEL_APPLICATION"); tail.setSourceRegion("CUSTOMER"); tail.setSourceOrderId(String.valueOf(appId)); tail.setSourceOrderNo(relabelNo);
|
||||
tail.setOverseasWarehouseId(order.getOverseasWarehouseId()); tail.setCurrentStatus("PENDING_ASSIGNMENT"); tail.setSubOrderStatus("RELABEL:RETURN_CARTON"); tail.setRelabelStatus("RETURN_CARTON"); tail.setPackageCount(selected.size());
|
||||
tail.setProductQuantity(Math.toIntExact(selected.stream().mapToLong(c -> value(c.getTotalQuantity())).sum())); tail.setCreatedAt(now); tail.setUpdatedAt(now); lastlegOrders.insert(tail);
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(appId); app.setCustomerId(customerId); app.setRelabelNo(relabelNo); app.setLastlegOrderId(tail.getId());
|
||||
app.setCustomerOrderNo(order.getReturnNo()); app.setOverseasWarehouseId(order.getOverseasWarehouseId()); app.setContactName(text(request.contactName())); app.setContactPhone(text(request.contactPhone())); app.setDeliveryMethod(text(request.deliveryMethod()) == null ? "TRUCK" : text(request.deliveryMethod()));
|
||||
app.setFulfillmentStatus("WAIT_RELABEL"); app.setRemark(text(request.remark())); app.setStatus("PENDING_PROCESSING"); app.setItemCount(selected.size()); app.setTotalPackageCount(selected.size()); app.setNotifiedPackageCount(selected.size()); app.setChargeAmount(java.math.BigDecimal.ZERO); app.setChargeCurrency("CNY"); app.setWalletStatus("NOT_REQUIRED"); app.setIdempotencyKey(key); app.setRequestHash(hash(request)); app.setCreatedBy(text(operatorId)); app.setCreatedAt(now); app.setUpdatedAt(now); relabelApplications.insert(app);
|
||||
for (ReturnInventoryCarton carton : selected) {
|
||||
for (ReturnInventoryCartonItem ci : cartonItems.listCarton(carton.getId())) {
|
||||
RelabelItem item = new RelabelItem(); item.setId(IdWorker.getId()); item.setApplicationId(appId); item.setCustomerId(customerId); item.setSkuCode(ci.getSkuCode()); item.setProductNameZh(ci.getProductNameZh()); item.setTotalPackageCount(1); item.setNotifiedPackageCount(1); item.setShipmentNo(order.getReturnNo()); item.setCartonMark(carton.getCartonMark()); item.setFbaCartonMark(carton.getFbaCartonMark()); item.setOperationType("CARTON_LABEL"); item.setReturnCartonId(carton.getId()); item.setSourceCartonMark(carton.getCartonMark()); item.setCreatedAt(now); relabelItems.insert(item);
|
||||
}
|
||||
carton.setRelabelApplicationId(appId); carton.setRelabelNo(relabelNo); carton.setRelabelStatus("PENDING"); carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now); cartons.updateById(carton);
|
||||
}
|
||||
order.setRelabelNo(relabelNo); order.setRelabelStatus("PENDING_PROCESSING"); orders.updateById(order);
|
||||
return new ReturnOrderResponses.RelabelCreated(relabelNo, appId, order.getReturnNo(), selected.stream().map(ReturnInventoryCarton::getCartonMark).toList(), app.getStatus());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PdaReturnReceiptResponses.Start start(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.Start request) {
|
||||
ReturnOrder order = activeOrder(String.valueOf(identity.overseasWarehouseId()), request.returnNo());
|
||||
|
||||
@@ -5,6 +5,8 @@ public final class OverseasPermissions {
|
||||
public static final String LAST_MILE_ORDER_LIST = "platform:overseas-warehouse:last-mile-order:list";
|
||||
public static final String LAST_MILE_ORDER_EDIT = "platform:overseas-warehouse:last-mile-order:edit";
|
||||
public static final String LAST_MILE_ORDER_EXPORT = "platform:overseas-warehouse:last-mile-order:export";
|
||||
public static final String WAREHOUSE_TRANSFER_APPROVE =
|
||||
"platform:overseas-warehouse:warehouse-transfer:approve";
|
||||
public static final String INBOUND_ORDER_LIST = "platform:overseas-warehouse:inbound-order:list";
|
||||
public static final String INBOUND_ORDER_CREATE = "platform:overseas-warehouse:inbound-order:create";
|
||||
public static final String INBOUND_ORDER_EDIT = "platform:overseas-warehouse:inbound-order:edit";
|
||||
|
||||
@@ -50,6 +50,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class LastlegOrderService {
|
||||
@@ -76,10 +77,16 @@ public class LastlegOrderService {
|
||||
@Resource private WarehouseResourceGateway warehouseResourceGateway;
|
||||
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||
return page(customerId, query, null);
|
||||
}
|
||||
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
|
||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||
new Page<>(query.page(), query.size()), filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt));
|
||||
new Page<>(query.page(), query.size()), filter(customerId, query, relabelNo).orderByDesc(LastlegOrder::getCreatedAt));
|
||||
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(page.getRecords());
|
||||
return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()))).toList(),
|
||||
Map<Long, String> dispatchStatuses = dispatchStatuses(page.getRecords());
|
||||
return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()),
|
||||
dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"))).toList(),
|
||||
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
@@ -261,7 +268,8 @@ public class LastlegOrderService {
|
||||
.map(value -> new LastlegOrderCommandDtos.ManualNodeView(
|
||||
value.getNodeCode(), value.getNodeAt(), value.getSource()))
|
||||
.sorted(java.util.Comparator.comparingInt(value -> MANUAL_NODE_CODES.indexOf(value.nodeCode()))).toList();
|
||||
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId())), manualNodes, cargo, history.stream()
|
||||
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId()),
|
||||
dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED")), manualNodes, cargo, history.stream()
|
||||
.map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(),
|
||||
value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream()
|
||||
.map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(),
|
||||
@@ -508,6 +516,10 @@ public class LastlegOrderService {
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
|
||||
return filter(customerId, query, null);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
|
||||
}
|
||||
@@ -516,6 +528,7 @@ public class LastlegOrderService {
|
||||
return new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
|
||||
.like(StringUtils.hasText(relabelNo), LastlegOrder::getRelabelOrderNo, relabelNo)
|
||||
.eq(StringUtils.hasText(query.orderType()), LastlegOrder::getOrderType, query.orderType())
|
||||
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
|
||||
.like(StringUtils.hasText(query.lxCode()), LastlegOrder::getLxCode, query.lxCode())
|
||||
@@ -558,18 +571,28 @@ public class LastlegOrderService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations) {
|
||||
private Map<Long, String> dispatchStatuses(List<LastlegOrder> orders) {
|
||||
if (orders.isEmpty()) return Map.of();
|
||||
List<LastlegOrderMapper.DispatchStatusRow> rows = orderMapper.findDispatchStatuses(
|
||||
orders.stream().map(LastlegOrder::getId).toList());
|
||||
if (rows == null) return Map.of();
|
||||
return rows.stream().collect(Collectors.toMap(LastlegOrderMapper.DispatchStatusRow::orderId,
|
||||
LastlegOrderMapper.DispatchStatusRow::dispatchStatus));
|
||||
}
|
||||
|
||||
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations,
|
||||
String dispatchStatus) {
|
||||
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
|
||||
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
|
||||
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
||||
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
||||
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
||||
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), o.getContainerStatus(),
|
||||
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(),
|
||||
o.getAbnormal(), o.getDetained(), o.getHoldChargeable(), o.getHoldReason(),
|
||||
Boolean.TRUE.equals(o.getManualNodeOverride()), Boolean.TRUE.equals(o.getManualNodeOverride()) ? "MANUAL" : "CONTAINER",
|
||||
o.getVersion() == null ? 0 : o.getVersion(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
|
||||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getRelabelOrderNo(), o.getRelabelStatus(),
|
||||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), dispatchStatus, o.getRelabelOrderNo(), o.getRelabelStatus(),
|
||||
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
|
||||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(),
|
||||
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
|
||||
|
||||
+7
@@ -36,6 +36,13 @@ public class MerchantWarehouseTransferController {
|
||||
return ApiResponse.success(service.detail(customer(user), transferNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/confirm-measurement")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> confirmMeasurement(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.CustomerConfirm request) {
|
||||
return ApiResponse.success(service.confirmMeasurement(customer(user), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.baiyue.lastleg.transfer.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/warehouse-transfers")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaWarehouseTransferController {
|
||||
@Resource private WarehouseTransferService service;
|
||||
|
||||
@GetMapping("/candidates")
|
||||
public ApiResponse<List<WarehouseTransferResponses.Summary>> candidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.pdaCandidates(identity.overseasWarehouseId()));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/palletize")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> palletize(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.Palletize request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.palletize(identity.operatorKey(), identity.overseasWarehouseId(),
|
||||
transferNo, request));
|
||||
}
|
||||
}
|
||||
+12
-8
@@ -3,12 +3,14 @@ package com.baiyue.lastleg.transfer.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -26,22 +28,31 @@ public class WarehouseTransferController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
public ApiResponse<PageResult<WarehouseTransferResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid WarehouseTransferRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), transferNo));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/carton-label")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Label> cartonLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
|
||||
return ApiResponse.success(service.cartonLabel(scope(user, customerId), transferNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/review")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.WAREHOUSE_TRANSFER_APPROVE + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> review(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.Review request) {
|
||||
requireApprovalPermission(user);
|
||||
return ApiResponse.success(service.review(scope(user, customerId), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
@@ -69,13 +80,6 @@ public class WarehouseTransferController {
|
||||
return ApiResponse.success(service.receive(scope(user, customerId), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
private void requireApprovalPermission(AuthenticatedUser user) {
|
||||
if (user == null || user.permissions() == null
|
||||
|| !user.permissions().contains("lastleg:warehouse-transfer:approve")) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Warehouse transfer approval permission is required");
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePermission(AuthenticatedUser user, String permission) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (user.permissions() == null || !user.permissions().contains(permission)) {
|
||||
|
||||
+49
-3
@@ -23,17 +23,34 @@ public final class WarehouseTransferRequests {
|
||||
@Size(max = 128) String fbaWarehouseCode,
|
||||
@NotBlank @Size(max = 1000) String deliveryAddress,
|
||||
@Positive int packageCount,
|
||||
@NotNull @DecimalMin(value = "0.0001") BigDecimal volumeCbm,
|
||||
@NotNull @DecimalMin(value = "0.0") BigDecimal volumeCbm,
|
||||
@NotBlank @Size(max = 64) String deliveryMethod,
|
||||
@NotNull LocalDateTime estimatedArrivalAt,
|
||||
LocalDateTime estimatedShipAt,
|
||||
@Size(max = 128) String fbaShipmentNo,
|
||||
@Size(max = 255) String trackingNo,
|
||||
@Size(max = 2000) String fbaBoxNos,
|
||||
@Size(max = 1000) String remark,
|
||||
boolean needRelabel,
|
||||
LocalDateTime labelDeadline,
|
||||
@Size(max = 128) String relabelContactName,
|
||||
@Size(max = 64) String relabelContactPhone,
|
||||
@DecimalMin(value = "0.01") BigDecimal relabelChargeAmount,
|
||||
@Size(max = 32) String relabelDeliveryMethod,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items
|
||||
) {}
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items,
|
||||
@Size(max = 1000) List<@Valid RelabelItem> relabelItems
|
||||
) {
|
||||
public Create(String inboundCode, List<Long> sourceLastlegOrderIds, String operatingWarehouseId,
|
||||
String fbaWarehouseCode, String deliveryAddress, int packageCount, BigDecimal volumeCbm,
|
||||
String deliveryMethod, LocalDateTime estimatedArrivalAt, String fbaShipmentNo,
|
||||
String trackingNo, String fbaBoxNos, String remark, boolean needRelabel,
|
||||
String idempotencyKey, List<Item> items) {
|
||||
this(inboundCode, sourceLastlegOrderIds, operatingWarehouseId, fbaWarehouseCode, deliveryAddress,
|
||||
packageCount, volumeCbm, deliveryMethod, estimatedArrivalAt, null, fbaShipmentNo, trackingNo,
|
||||
fbaBoxNos, remark, needRelabel, null, null, null, null, null, idempotencyKey, items, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record Review(@NotBlank @Size(max = 32) String decision,
|
||||
@Size(max = 1000) String remark) {}
|
||||
@@ -52,6 +69,24 @@ public final class WarehouseTransferRequests {
|
||||
public record Outbound(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record Palletize(
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 100) List<@Valid Pallet> pallets
|
||||
) {}
|
||||
|
||||
public record Pallet(
|
||||
@Size(max = 128) String palletCode,
|
||||
@Positive int cartonCount,
|
||||
@NotNull @DecimalMin("0.000001") BigDecimal volumeCbm,
|
||||
@NotNull @DecimalMin("0.01") BigDecimal grossWeightKg,
|
||||
@NotEmpty @Size(max = 500) List<@Valid PalletItem> items
|
||||
) {}
|
||||
|
||||
public record PalletItem(@NotNull Long transferItemId, @Positive int packageCount) {}
|
||||
|
||||
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record Receipt(
|
||||
@NotNull Long sourceInventoryPositionId,
|
||||
@NotBlank @Size(max = 128) String destinationOverseasWarehouseId,
|
||||
@@ -76,6 +111,17 @@ public final class WarehouseTransferRequests {
|
||||
@Size(max = 255) String shippingMark
|
||||
) {}
|
||||
|
||||
public record RelabelItem(
|
||||
@NotBlank @Size(max = 32) String operationType,
|
||||
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> customerCartonMarks,
|
||||
@NotBlank @Size(max = 128) String originalSku,
|
||||
@Size(max = 128) String newSku,
|
||||
@Positive int quantity,
|
||||
@Size(max = 255) String fbaCartonMark,
|
||||
@Size(max = 128) String destinationWarehouseCode,
|
||||
@Size(max = 1000) String remark
|
||||
) {}
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String transferNo, String inboundCode, String operatingWarehouseId,
|
||||
String fbaWarehouseCode, String status, Boolean needRelabel,
|
||||
|
||||
+19
-5
@@ -1,12 +1,23 @@
|
||||
package com.baiyue.lastleg.transfer.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
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 Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark) {}
|
||||
public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status) {}
|
||||
public record Label(String transferNo, List<Carton> cartons, String printableHtml) {}
|
||||
public record PalletItem(Long transferItemId, String skuCode, int packageCount) {}
|
||||
public record Pallet(Long id, int sequenceNo, Long warehousePalletId, String palletNo,
|
||||
Long warehouseLocationId, String warehouseLocationCode, int cartonCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
BigDecimal grossWeightKg, BigDecimal volumeCbm, String status,
|
||||
List<PalletItem> items, LocalDateTime completedAt) {}
|
||||
public record RelabelBinding(String operationType, String customerCartonMark, String originalSku,
|
||||
String newSku, int quantity, String fbaCartonMark,
|
||||
String destinationWarehouseCode, String remark) {}
|
||||
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,
|
||||
@@ -19,9 +30,12 @@ public final class WarehouseTransferResponses {
|
||||
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
||||
String status,
|
||||
boolean needRelabel, Long relabelApplicationId, String relabelNo, String relabelStatus,
|
||||
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
|
||||
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
|
||||
int packageCount, String deliveryMethod, LocalDateTime estimatedArrivalAt,
|
||||
LocalDateTime estimatedShipAt,
|
||||
String fbaShipmentNo, String trackingNo, String fbaBoxNos, String remark,
|
||||
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons, List<Allocation> allocations,
|
||||
LocalDateTime outboundAt, LocalDateTime createdAt) {}
|
||||
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons,
|
||||
List<Pallet> pallets, List<RelabelBinding> relabelBindings,
|
||||
List<Allocation> allocations, LocalDateTime customerConfirmedAt,
|
||||
LocalDateTime actualShipAt, LocalDateTime createdAt) {}
|
||||
}
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ public class WarehouseTransferCarton {
|
||||
private Long transferItemId;
|
||||
private String customerId;
|
||||
private String cartonMark;
|
||||
private String customerCartonMark;
|
||||
private Integer cartonIndex;
|
||||
private Integer totalCartons;
|
||||
private String status;
|
||||
|
||||
+12
@@ -22,19 +22,31 @@ public class WarehouseTransferOrder {
|
||||
private Integer packageCount;
|
||||
private String deliveryMethod;
|
||||
private LocalDateTime estimatedArrivalAt;
|
||||
private LocalDateTime estimatedShipAt;
|
||||
private String fbaShipmentNo;
|
||||
private String trackingNo;
|
||||
private String fbaBoxNos;
|
||||
private String remark;
|
||||
private Boolean needRelabel;
|
||||
private LocalDateTime labelDeadline;
|
||||
private String relabelContactName;
|
||||
private String relabelContactPhone;
|
||||
private java.math.BigDecimal relabelChargeAmount;
|
||||
private String relabelDeliveryMethod;
|
||||
private Long relabelApplicationId;
|
||||
private String relabelNo;
|
||||
private LocalDateTime relabelCompletedAt;
|
||||
private String status;
|
||||
private String allocationIdempotencyKey;
|
||||
private String allocationRequestHash;
|
||||
private String outboundIdempotencyKey;
|
||||
private String outboundRequestHash;
|
||||
private LocalDateTime outboundAt;
|
||||
private String palletizeIdempotencyKey;
|
||||
private String palletizeRequestHash;
|
||||
private String confirmationIdempotencyKey;
|
||||
private String confirmationRequestHash;
|
||||
private LocalDateTime customerConfirmedAt;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String createdBy;
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_warehouse_transfer_pallet")
|
||||
public class WarehouseTransferPallet {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long transferOrderId;
|
||||
private String customerId;
|
||||
private Integer sequenceNo;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
private Long warehouseLocationId;
|
||||
private String warehouseLocationCode;
|
||||
private Long warehouseResourceLockId;
|
||||
private Integer cartonCount;
|
||||
private BigDecimal lengthCm;
|
||||
private BigDecimal widthCm;
|
||||
private BigDecimal heightCm;
|
||||
private BigDecimal grossWeightKg;
|
||||
private BigDecimal volumeCbm;
|
||||
private String status;
|
||||
private LocalDateTime completedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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_pallet_item")
|
||||
public class WarehouseTransferPalletItem {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long transferOrderId;
|
||||
private Long transferPalletId;
|
||||
private Long transferItemId;
|
||||
private String customerId;
|
||||
private Integer packageCount;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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_relabel_binding")
|
||||
public class WarehouseTransferRelabelBinding {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long transferOrderId;
|
||||
private String customerId;
|
||||
private String operationType;
|
||||
private String customerCartonMark;
|
||||
private String originalSku;
|
||||
private String newSku;
|
||||
private Integer quantity;
|
||||
private String fbaCartonMark;
|
||||
private String destinationWarehouseCode;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+13
@@ -4,6 +4,7 @@ import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
@Mapper public interface WarehouseTransferOrderMapper extends BaseMapper<WarehouseTransferOrder> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
WarehouseTransferOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@@ -11,4 +12,16 @@ import org.apache.ibatis.annotations.Select;
|
||||
WarehouseTransferOrder findNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND transfer_no=#{transferNo} LIMIT 1 FOR UPDATE")
|
||||
WarehouseTransferOrder lockNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE transfer_no=#{transferNo} AND operating_warehouse_id=#{warehouseId} LIMIT 1 FOR UPDATE")
|
||||
WarehouseTransferOrder lockForPda(@Param("transferNo") String transferNo, @Param("warehouseId") String warehouseId);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
WarehouseTransferOrder lockByRelabelNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE operating_warehouse_id=#{warehouseId} AND status='WAIT_PDA_RECEIVING' ORDER BY created_at,id")
|
||||
java.util.List<WarehouseTransferOrder> pdaCandidates(@Param("warehouseId") String warehouseId);
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL")
|
||||
int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND outbound_at IS NULL")
|
||||
int markOutboundByRelabel(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPalletItem;
|
||||
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 WarehouseTransferPalletItemMapper extends BaseMapper<WarehouseTransferPalletItem> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_pallet_item WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY transfer_pallet_id,id")
|
||||
List<WarehouseTransferPalletItem> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
|
||||
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 WarehouseTransferPalletMapper extends BaseMapper<WarehouseTransferPallet> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_pallet WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY sequence_no")
|
||||
List<WarehouseTransferPallet> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding;
|
||||
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 WarehouseTransferRelabelBindingMapper extends BaseMapper<WarehouseTransferRelabelBinding> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_relabel_binding WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY id")
|
||||
List<WarehouseTransferRelabelBinding> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
}
|
||||
+466
-9
@@ -20,6 +20,20 @@ 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.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundStockReceiptMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||
@@ -29,6 +43,9 @@ 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.entity.WarehouseTransferPallet;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPalletItem;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding;
|
||||
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferItemMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
|
||||
@@ -36,6 +53,9 @@ import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMa
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferSourceOrderMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletItemMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferRelabelBindingMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -53,7 +73,12 @@ import java.util.HexFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -68,11 +93,18 @@ public class WarehouseTransferService {
|
||||
public static final String OUTBOUNDED = "OUTBOUNDED";
|
||||
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
|
||||
public static final String RECEIVED = "RECEIVED";
|
||||
public static final String WAIT_PDA_RECEIVING = "WAIT_PDA_RECEIVING";
|
||||
public static final String WAIT_CUSTOMER_CONFIRMATION = "WAIT_CUSTOMER_CONFIRMATION";
|
||||
public static final String READY_FOR_DISPATCH = "READY_FOR_DISPATCH";
|
||||
public static final String WAIT_RELABEL = "WAIT_RELABEL";
|
||||
|
||||
@Resource private WarehouseTransferOrderMapper transfers;
|
||||
@Resource private WarehouseTransferItemMapper transferItems;
|
||||
@Resource private WarehouseTransferCartonMapper transferCartons;
|
||||
@Resource private WarehouseTransferSourceOrderMapper sourceOrders;
|
||||
@Resource private WarehouseTransferPalletMapper transferPallets;
|
||||
@Resource private WarehouseTransferPalletItemMapper transferPalletItems;
|
||||
@Resource private WarehouseTransferRelabelBindingMapper relabelBindings;
|
||||
@Resource private WarehouseTransferInventoryAllocationMapper inventoryAllocations;
|
||||
@Resource private WarehouseTransferReceiptMapper transferReceipts;
|
||||
@Resource private InboundSnapshotMapper inboundSnapshots;
|
||||
@@ -81,6 +113,13 @@ public class WarehouseTransferService {
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelApplicationService relabelService;
|
||||
@Resource private WarehouseResourceGateway warehouseResources;
|
||||
@Resource private InboundPlanMapper inboundPlans;
|
||||
@Resource private InboundPlanLineMapper inboundPlanLines;
|
||||
@Resource private InboundPlanPalletMapper inboundPlanPallets;
|
||||
@Resource private InboundPlanPalletAllocationMapper inboundPlanAllocations;
|
||||
@Resource private InboundStockReceiptMapper stockReceipts;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private InventoryCommandMapper inventoryCommands;
|
||||
@@ -93,6 +132,7 @@ public class WarehouseTransferService {
|
||||
if (itemPackages != request.packageCount()) {
|
||||
throw badRequest("Cargo item package total must equal transfer package count");
|
||||
}
|
||||
validateRelabelRequest(request);
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
|
||||
@@ -142,10 +182,16 @@ public class WarehouseTransferService {
|
||||
transfer.setFbaWarehouseCode(trim(request.fbaWarehouseCode()));
|
||||
transfer.setDeliveryAddress(request.deliveryAddress().trim()); transfer.setPackageCount(request.packageCount());
|
||||
transfer.setDeliveryMethod(request.deliveryMethod().trim()); transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
|
||||
transfer.setEstimatedShipAt(request.estimatedShipAt());
|
||||
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.setLabelDeadline(request.labelDeadline());
|
||||
transfer.setRelabelContactName(trim(request.relabelContactName()));
|
||||
transfer.setRelabelContactPhone(trim(request.relabelContactPhone()));
|
||||
transfer.setRelabelChargeAmount(request.relabelChargeAmount());
|
||||
transfer.setRelabelDeliveryMethod(trim(request.relabelDeliveryMethod()));
|
||||
transfer.setIdempotencyKey(key); transfer.setRequestHash(requestHash); transfer.setCreatedBy(trim(operatorId));
|
||||
transfer.setCreatedAt(now); transfer.setUpdatedAt(now);
|
||||
transfers.insert(transfer);
|
||||
@@ -164,14 +210,17 @@ public class WarehouseTransferService {
|
||||
sourceOrders.insert(link);
|
||||
}
|
||||
|
||||
if (request.needRelabel() && request.relabelItems() != null) insertRelabelBindings(customerId, transferId, request.relabelItems(), now);
|
||||
|
||||
int cartonIndex = 0;
|
||||
for (int index = 0; index < request.items().size(); index++) {
|
||||
WarehouseTransferRequests.Item source = request.items().get(index);
|
||||
long itemId = insertTransferItem(customerId, transferId, source, now);
|
||||
List<String> customerMarks = splitMarks(source.shippingMark());
|
||||
for (int itemCarton = 0; itemCarton < source.packageCount(); itemCarton++) {
|
||||
cartonIndex++;
|
||||
insertTransferCarton(customerId, transferId, itemId, transferNo,
|
||||
cartonIndex, request.packageCount(), now);
|
||||
cartonIndex, request.packageCount(), itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null, now);
|
||||
}
|
||||
insertOrderDetail(customerId, order.getId(), transferNo, index, source, request, now);
|
||||
}
|
||||
@@ -213,16 +262,36 @@ public class WarehouseTransferService {
|
||||
List<WarehouseTransferResponses.Item> items = transferItems.selectList(
|
||||
new LambdaQueryWrapper<WarehouseTransferItem>().eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId))
|
||||
.stream().map(value -> new WarehouseTransferResponses.Item(value.getSkuCode(), value.getProductNameZh(),
|
||||
.stream().map(value -> new WarehouseTransferResponses.Item(value.getId(), value.getSkuCode(), value.getProductNameZh(),
|
||||
value.getPackageCount(), value.getShippingMark())).toList();
|
||||
Map<Long, WarehouseTransferResponses.Item> itemById = items.stream()
|
||||
.collect(Collectors.toMap(WarehouseTransferResponses.Item::id, Function.identity()));
|
||||
List<WarehouseTransferResponses.SourceOrder> sources = sourceOrders.selectList(
|
||||
new LambdaQueryWrapper<WarehouseTransferSourceOrder>().eq(WarehouseTransferSourceOrder::getCustomerId, customerId)
|
||||
.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(),
|
||||
.stream().map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(),
|
||||
value.getCartonIndex(), value.getTotalCartons(), value.getStatus())).toList();
|
||||
List<WarehouseTransferPalletItem> palletItemRows = transferPalletItems == null
|
||||
? List.of() : transferPalletItems.listOrder(customerId, transfer.getId());
|
||||
Map<Long, List<WarehouseTransferPalletItem>> palletItemsByPallet = palletItemRows.stream()
|
||||
.collect(Collectors.groupingBy(WarehouseTransferPalletItem::getTransferPalletId));
|
||||
List<WarehouseTransferResponses.Pallet> pallets = transferPallets == null ? List.of() : transferPallets.listOrder(customerId, transfer.getId()).stream()
|
||||
.map(value -> new WarehouseTransferResponses.Pallet(value.getId(), value.getSequenceNo(),
|
||||
value.getWarehousePalletId(), value.getPalletNo(), value.getWarehouseLocationId(),
|
||||
value.getWarehouseLocationCode(), value.getCartonCount(), value.getLengthCm(), value.getWidthCm(),
|
||||
value.getHeightCm(), value.getGrossWeightKg(), value.getVolumeCbm(), value.getStatus(),
|
||||
palletItemsByPallet.getOrDefault(value.getId(), List.of()).stream().map(row -> {
|
||||
WarehouseTransferResponses.Item item = itemById.get(row.getTransferItemId());
|
||||
return new WarehouseTransferResponses.PalletItem(row.getTransferItemId(),
|
||||
item == null ? null : item.skuCode(), row.getPackageCount());
|
||||
}).toList(), value.getCompletedAt())).toList();
|
||||
List<WarehouseTransferResponses.RelabelBinding> bindings = relabelBindings == null ? List.of() : relabelBindings.listOrder(customerId, transfer.getId())
|
||||
.stream().map(value -> new WarehouseTransferResponses.RelabelBinding(value.getOperationType(),
|
||||
value.getCustomerCartonMark(), value.getOriginalSku(), value.getNewSku(), value.getQuantity(),
|
||||
value.getFbaCartonMark(), value.getDestinationWarehouseCode(), value.getRemark())).toList();
|
||||
Map<Long, WarehouseTransferReceipt> receiptByAllocation = transferReceipts.selectList(
|
||||
new LambdaQueryWrapper<WarehouseTransferReceipt>().eq(WarehouseTransferReceipt::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferReceipt::getTransferOrderId, transfer.getId()))
|
||||
@@ -252,9 +321,32 @@ public class WarehouseTransferService {
|
||||
Boolean.TRUE.equals(transfer.getNeedRelabel()), transfer.getRelabelApplicationId(), transfer.getRelabelNo(),
|
||||
relabelStatus,
|
||||
transfer.getOperatingWarehouseId(), transfer.getFbaWarehouseCode(), transfer.getDeliveryAddress(),
|
||||
transfer.getPackageCount(), transfer.getDeliveryMethod(), transfer.getEstimatedArrivalAt(), transfer.getFbaShipmentNo(),
|
||||
transfer.getTrackingNo(), transfer.getFbaBoxNos(), transfer.getRemark(), sources, items, cartons, allocations,
|
||||
transfer.getOutboundAt(), transfer.getCreatedAt());
|
||||
transfer.getPackageCount(), transfer.getDeliveryMethod(), transfer.getEstimatedArrivalAt(),
|
||||
transfer.getEstimatedShipAt(), transfer.getFbaShipmentNo(), transfer.getTrackingNo(), transfer.getFbaBoxNos(),
|
||||
transfer.getRemark(), sources, items, cartons, pallets, bindings, allocations,
|
||||
transfer.getCustomerConfirmedAt(), transfer.getOutboundAt(), transfer.getCreatedAt());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public WarehouseTransferResponses.Label cartonLabel(String customerId, String transferNo) {
|
||||
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream()
|
||||
.map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus())).toList();
|
||||
StringBuilder labels = new StringBuilder();
|
||||
for (WarehouseTransferResponses.Carton carton : cartons) {
|
||||
labels.append("<section class=label><div class=title>仓储转运箱唛</div><div class=mark>")
|
||||
.append(escape(carton.cartonMark())).append("</div><div class=meta>")
|
||||
.append(carton.cartonIndex()).append(" / ").append(carton.totalCartons()).append(" 转运单号:")
|
||||
.append(escape(transferNo)).append("</div>");
|
||||
if (text(carton.customerCartonMark()) != null) labels.append("<div class=customer>客户箱唛:")
|
||||
.append(escape(carton.customerCartonMark())).append("</div>");
|
||||
labels.append("</section>");
|
||||
}
|
||||
String html = "<!doctype html><html><head><meta charset=utf-8><title>" + escape(transferNo) + "-箱唛</title><style>"
|
||||
+ "@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:12mm 0 6mm;font-size:30px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.meta,.customer{font-size:12px;margin-top:3mm}</style></head><body>"
|
||||
+ labels + "</body></html>";
|
||||
return new WarehouseTransferResponses.Label(transferNo, cartons, html);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -269,15 +361,18 @@ public class WarehouseTransferService {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
if (!PENDING_REVIEW.equals(transfer.getStatus())) {
|
||||
if (next.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
||||
if (next.equals(transfer.getStatus()) || APPROVED.equals(next)
|
||||
&& Set.of(WAIT_PDA_RECEIVING, WAIT_CUSTOMER_CONFIRMATION, READY_FOR_DISPATCH, WAIT_RELABEL)
|
||||
.contains(transfer.getStatus())) return detail(customerId, transferNo);
|
||||
throw conflict("Warehouse transfer order was already reviewed");
|
||||
}
|
||||
LastlegOrder order = orders.selectById(transfer.getLastlegOrderId());
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String before = order.getCurrentStatus();
|
||||
transfer.setStatus(next); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
boolean incomingTransit = transfer.getTransferNo().equals(transfer.getInboundCode());
|
||||
String transferStatus = APPROVED.equals(next) && incomingTransit ? WAIT_PDA_RECEIVING : next;
|
||||
transfer.setStatus(transferStatus); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
String orderStatus = APPROVED.equals(next)
|
||||
? (incomingTransit
|
||||
? ("WAREHOUSE_PICKUP".equals(transfer.getDeliveryMethod()) ? "WAIT_PICKUP" : "WAIT_DELIVERY")
|
||||
@@ -294,6 +389,191 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail palletize(String operatorId, long overseasWarehouseId,
|
||||
String transferNo,
|
||||
WarehouseTransferRequests.Palletize request) {
|
||||
WarehouseTransferOrder transfer = transfers.lockForPda(transferNo, String.valueOf(overseasWarehouseId));
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
if (!String.valueOf(overseasWarehouseId).equals(transfer.getOperatingWarehouseId())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Warehouse transfer belongs to another warehouse");
|
||||
}
|
||||
String customerId = transfer.getCustomerId();
|
||||
String requestHash = hash(request);
|
||||
if (WAIT_CUSTOMER_CONFIRMATION.equals(transfer.getStatus())
|
||||
&& request.idempotencyKey().equals(transfer.getPalletizeIdempotencyKey())) {
|
||||
if (!requestHash.equals(transfer.getPalletizeRequestHash())) {
|
||||
throw conflict("Palletization idempotency key was reused with different data");
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
if (!WAIT_PDA_RECEIVING.equals(transfer.getStatus())) {
|
||||
throw conflict("Warehouse transfer is not awaiting PDA receiving");
|
||||
}
|
||||
List<WarehouseTransferItem> items = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId));
|
||||
Map<Long, WarehouseTransferItem> itemById = items.stream()
|
||||
.collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity()));
|
||||
Map<Long, Integer> totals = new java.util.HashMap<>();
|
||||
int palletCartons = 0;
|
||||
for (WarehouseTransferRequests.Pallet pallet : request.pallets()) {
|
||||
int itemTotal = pallet.items().stream().mapToInt(WarehouseTransferRequests.PalletItem::packageCount).sum();
|
||||
if (itemTotal != pallet.cartonCount()) throw badRequest("Each pallet carton count must equal its SKU carton total");
|
||||
palletCartons = Math.addExact(palletCartons, pallet.cartonCount());
|
||||
Set<Long> uniqueItems = new HashSet<>();
|
||||
for (WarehouseTransferRequests.PalletItem item : pallet.items()) {
|
||||
if (!uniqueItems.add(item.transferItemId()) || !itemById.containsKey(item.transferItemId())) {
|
||||
throw badRequest("Pallet SKU rows must be unique and belong to this transfer");
|
||||
}
|
||||
totals.merge(item.transferItemId(), item.packageCount(), Math::addExact);
|
||||
}
|
||||
}
|
||||
if (palletCartons != transfer.getPackageCount()) throw badRequest("Pallet carton total must equal transfer package count");
|
||||
for (WarehouseTransferItem item : items) {
|
||||
if (totals.getOrDefault(item.getId(), 0) != item.getPackageCount()) {
|
||||
throw badRequest("Palletized carton total does not match declared SKU " + item.getSkuCode());
|
||||
}
|
||||
}
|
||||
|
||||
String batchKey = "WAREHOUSE_TRANSFER:" + transferNo + ":PALLETIZE";
|
||||
List<WarehouseResourceGateway.PalletBatchItemRequest> batchItems = new ArrayList<>();
|
||||
for (int index = 0; index < request.pallets().size(); index++) {
|
||||
WarehouseTransferRequests.Pallet pallet = request.pallets().get(index);
|
||||
batchItems.add(new WarehouseResourceGateway.PalletBatchItemRequest(Long.parseLong(customerId),
|
||||
transferNo + ":" + (index + 1), transferNo, index + 1, "STANDARD",
|
||||
trim(transfer.getFbaWarehouseCode()), trim(pallet.palletCode())));
|
||||
}
|
||||
WarehouseResourceGateway.PalletBatchResponse ensured = warehouseResources.ensurePalletBatch(
|
||||
new WarehouseResourceGateway.PalletBatchEnsure(overseasWarehouseId, batchKey, batchItems));
|
||||
WarehouseResourceGateway.PalletBatchResponse confirmed = warehouseResources.confirmPalletBatch(
|
||||
new WarehouseResourceGateway.PalletBatchCommand(overseasWarehouseId, batchKey,
|
||||
request.idempotencyKey() + ":CONFIRM"));
|
||||
if (confirmed == null || confirmed.items() == null || confirmed.items().size() != request.pallets().size()) {
|
||||
throw conflict("Warehouse pallet batch did not return every pallet");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (int index = 0; index < request.pallets().size(); index++) {
|
||||
int sequenceNo = index + 1;
|
||||
WarehouseTransferRequests.Pallet source = request.pallets().get(index);
|
||||
WarehouseResourceGateway.PalletBatchItem resource = confirmed.items().stream()
|
||||
.filter(value -> value.sequenceNo() == sequenceNo).findFirst()
|
||||
.orElseThrow(() -> conflict("Warehouse pallet sequence is incomplete"));
|
||||
WarehouseResourceGateway.WarehouseLocationReference location = warehouseResources.resolveLocation(
|
||||
overseasWarehouseId, resource.locationId());
|
||||
WarehouseTransferPallet pallet = new WarehouseTransferPallet();
|
||||
pallet.setId(IdWorker.getId()); pallet.setTransferOrderId(transfer.getId()); pallet.setCustomerId(customerId);
|
||||
pallet.setSequenceNo(sequenceNo); pallet.setWarehousePalletId(resource.palletId());
|
||||
pallet.setPalletNo(resource.palletCode()); pallet.setWarehouseLocationId(resource.locationId());
|
||||
pallet.setWarehouseLocationCode(location.locationCode()); pallet.setWarehouseResourceLockId(resource.resourceLockId());
|
||||
// 转运测量以打托后的托盘为单位,体积由 PDA 直接录入托盘总体积,不再按单箱尺寸换算。
|
||||
// 旧表字段仍为 NOT NULL;按托模式不采集长宽高,以 0 兼容存储且不得参与总体积计算。
|
||||
pallet.setLengthCm(BigDecimal.ZERO); pallet.setWidthCm(BigDecimal.ZERO); pallet.setHeightCm(BigDecimal.ZERO);
|
||||
pallet.setCartonCount(source.cartonCount()); pallet.setGrossWeightKg(source.grossWeightKg());
|
||||
pallet.setVolumeCbm(source.volumeCbm().setScale(6, RoundingMode.HALF_UP));
|
||||
pallet.setStatus("COMPLETED"); pallet.setCompletedAt(now); pallet.setCreatedAt(now); pallet.setUpdatedAt(now);
|
||||
transferPallets.insert(pallet);
|
||||
for (WarehouseTransferRequests.PalletItem sourceItem : source.items()) {
|
||||
WarehouseTransferPalletItem row = new WarehouseTransferPalletItem();
|
||||
row.setId(IdWorker.getId()); row.setTransferOrderId(transfer.getId()); row.setTransferPalletId(pallet.getId());
|
||||
row.setTransferItemId(sourceItem.transferItemId()); row.setCustomerId(customerId);
|
||||
row.setPackageCount(sourceItem.packageCount()); row.setCreatedAt(now); transferPalletItems.insert(row);
|
||||
}
|
||||
}
|
||||
transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim());
|
||||
transfer.setPalletizeRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
|
||||
order.setCurrentStatus(WAIT_CUSTOMER_CONFIRMATION); order.setPalletCount(request.pallets().size());
|
||||
order.setVolumeCbm(transferPallets.listOrder(customerId, transfer.getId()).stream()
|
||||
.map(WarehouseTransferPallet::getVolumeCbm).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + WAIT_CUSTOMER_CONFIRMATION); order.setUpdatedAt(now); orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
"PDA receiving measurement and palletization completed", now);
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail confirmMeasurement(String customerId, String transferNo,
|
||||
String operatorId,
|
||||
WarehouseTransferRequests.CustomerConfirm request) {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
String requestHash = hash(request);
|
||||
if (Set.of(READY_FOR_DISPATCH, WAIT_RELABEL, OUTBOUNDED).contains(transfer.getStatus())
|
||||
&& request.idempotencyKey().equals(transfer.getConfirmationIdempotencyKey())) {
|
||||
if (!requestHash.equals(transfer.getConfirmationRequestHash())) {
|
||||
throw conflict("Confirmation idempotency key was reused with different data");
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
if (!WAIT_CUSTOMER_CONFIRMATION.equals(transfer.getStatus())) {
|
||||
throw conflict("Warehouse transfer measurement is not awaiting customer confirmation");
|
||||
}
|
||||
List<WarehouseTransferItem> items = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId));
|
||||
Map<Long, WarehouseTransferItem> itemById = items.stream()
|
||||
.collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity()));
|
||||
List<WarehouseTransferPallet> pallets = transferPallets.listOrder(customerId, transfer.getId());
|
||||
List<WarehouseTransferPalletItem> palletItems = transferPalletItems.listOrder(customerId, transfer.getId());
|
||||
WarehouseResourceGateway.WarehouseReference warehouse = warehouseResources.requireActiveSharedWarehouse(
|
||||
Long.parseLong(transfer.getOperatingWarehouseId()));
|
||||
Map<String, List<ConfirmedStockSource>> stockSourcesBySku = new java.util.LinkedHashMap<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (WarehouseTransferPallet pallet : pallets) {
|
||||
for (WarehouseTransferPalletItem row : palletItems.stream()
|
||||
.filter(value -> value.getTransferPalletId().equals(pallet.getId())).toList()) {
|
||||
WarehouseTransferItem item = itemById.get(row.getTransferItemId());
|
||||
String operationKey = "WAREHOUSE_TRANSFER_STOCK_IN:" + transferNo + ":" + pallet.getSequenceNo() + ":" + item.getId();
|
||||
InventoryCommandResponse result = inventory.stockIn(customerId, operatorId,
|
||||
new InventoryCommandRequests.StockIn(customerId, "客户-" + customerId,
|
||||
transfer.getOperatingWarehouseId(), warehouse.warehouseCode(), warehouse.warehouseName(),
|
||||
null, trim(transfer.getFbaWarehouseCode()), trim(transfer.getFbaWarehouseCode()),
|
||||
transfer.getInboundOrderId(), transferNo, transferNo, item.getSkuCode(), item.getProductNameZh(), null,
|
||||
pallet.getWarehousePalletId(), pallet.getPalletNo(), transferNo, pallet.getWarehouseLocationId(),
|
||||
pallet.getWarehouseLocationCode(), row.getPackageCount(), transferNo,
|
||||
"WAREHOUSE_TRANSFER_CONFIRMED:" + transferNo, operationKey, trim(request.remark())));
|
||||
stockSourcesBySku.computeIfAbsent(item.getSkuCode(), ignored -> new ArrayList<>())
|
||||
.add(new ConfirmedStockSource(result.balanceId(), result.positionId(), row.getPackageCount()));
|
||||
InboundStockReceipt receipt = new InboundStockReceipt();
|
||||
receipt.setId(IdWorker.getId()); receipt.setCustomerId(customerId);
|
||||
// 转运收货没有独立 PDA 入仓任务,使用本业务链的稳定主键填充通用收货快照维度。
|
||||
receipt.setPlanId(transfer.getInboundOrderId()); receipt.setTaskId(transfer.getId());
|
||||
receipt.setAllocationId(row.getId()); receipt.setLineId(item.getId());
|
||||
receipt.setOrderId(transfer.getLastlegOrderId());
|
||||
receipt.setInboundCode(transferNo); receipt.setSkuCode(item.getSkuCode()); receipt.setProductNameZh(item.getProductNameZh());
|
||||
receipt.setWarehousePalletId(pallet.getWarehousePalletId()); receipt.setPalletNo(pallet.getPalletNo());
|
||||
receipt.setWarehouseLocationId(pallet.getWarehouseLocationId()); receipt.setWarehouseLocationCode(pallet.getWarehouseLocationCode());
|
||||
receipt.setBatchNo(transferNo); receipt.setReceivedPackages(Long.valueOf(row.getPackageCount()));
|
||||
receipt.setInventoryBalanceId(result.balanceId()); receipt.setInventoryPositionId(result.positionId());
|
||||
receipt.setInventoryOperationKey(operationKey); receipt.setReceivedAt(now); receipt.setCreatedAt(now); stockReceipts.insert(receipt);
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
RelabelResponses.Detail relabel = createLinkedRelabel(customerId, operatorId, transfer, itemById,
|
||||
stockSourcesBySku);
|
||||
transfer.setRelabelApplicationId(relabel.application().id());
|
||||
transfer.setRelabelNo(relabel.application().relabelNo()); transfer.setStatus(WAIT_RELABEL);
|
||||
} else {
|
||||
if (!StringUtils.hasText(transfer.getFbaWarehouseCode())) {
|
||||
throw conflict("A destination warehouse is required before dispatch");
|
||||
}
|
||||
createDispatchPlan(customerId, transfer, items, pallets, palletItems, now);
|
||||
transfer.setStatus(READY_FOR_DISPATCH);
|
||||
}
|
||||
transfer.setConfirmationIdempotencyKey(request.idempotencyKey().trim());
|
||||
transfer.setConfirmationRequestHash(requestHash); transfer.setCustomerConfirmedAt(now); transfer.setUpdatedAt(now);
|
||||
transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
|
||||
order.setCurrentStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? WAIT_RELABEL : READY_FOR_DISPATCH);
|
||||
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_PROCESSING" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
"Customer confirmed PDA measurements; actual inventory stocked in", now);
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail allocate(String customerId, String transferNo, String operatorId,
|
||||
WarehouseTransferRequests.Allocate request) {
|
||||
@@ -490,6 +770,182 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<WarehouseTransferResponses.Summary> pdaCandidates(long overseasWarehouseId) {
|
||||
return transfers.pdaCandidates(String.valueOf(overseasWarehouseId)).stream().map(value ->
|
||||
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
|
||||
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
|
||||
value.getFbaWarehouseCode(), value.getPackageCount(), value.getDeliveryMethod(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||
}
|
||||
|
||||
private void validateRelabelRequest(WarehouseTransferRequests.Create request) {
|
||||
if (!request.needRelabel()) {
|
||||
if (request.relabelItems() != null && !request.relabelItems().isEmpty()) {
|
||||
throw badRequest("Relabel items require needRelabel=true");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (request.relabelItems() == null || request.relabelItems().isEmpty()) return;
|
||||
if (request.labelDeadline() == null || !StringUtils.hasText(request.relabelContactName())
|
||||
|| !StringUtils.hasText(request.relabelContactPhone()) || request.relabelChargeAmount() == null) {
|
||||
throw badRequest("Relabel deadline, contact and charge are required");
|
||||
}
|
||||
Set<String> declaredSkus = request.items().stream().map(item -> item.skuCode().trim()).collect(Collectors.toSet());
|
||||
Set<String> marks = new HashSet<>();
|
||||
for (WarehouseTransferRequests.RelabelItem row : request.relabelItems()) {
|
||||
String type = row.operationType().trim().toUpperCase();
|
||||
if (!Set.of("CARTON_LABEL", "SKU_LABEL", "CARTON_AND_SKU").contains(type)) {
|
||||
throw badRequest("Unsupported relabel operation type");
|
||||
}
|
||||
if (!declaredSkus.contains(row.originalSku().trim())) {
|
||||
throw badRequest("Relabel SKU must exist in transfer cargo: " + row.originalSku());
|
||||
}
|
||||
if (("CARTON_LABEL".equals(type) || "CARTON_AND_SKU".equals(type))
|
||||
&& (!StringUtils.hasText(row.fbaCartonMark()) || !StringUtils.hasText(row.destinationWarehouseCode()))) {
|
||||
throw badRequest("Carton relabel requires new FBA carton mark and destination warehouse");
|
||||
}
|
||||
if (("SKU_LABEL".equals(type) || "CARTON_AND_SKU".equals(type)) && !StringUtils.hasText(row.newSku())) {
|
||||
throw badRequest("SKU relabel requires a new SKU");
|
||||
}
|
||||
for (String mark : row.customerCartonMarks()) {
|
||||
if (!marks.add(mark.trim().toUpperCase())) throw badRequest("Customer carton marks must be unique");
|
||||
}
|
||||
}
|
||||
if (marks.size() > request.packageCount()) throw badRequest("Relabel carton marks cannot exceed transfer cartons");
|
||||
}
|
||||
|
||||
private void insertRelabelBindings(String customerId, long transferId,
|
||||
List<WarehouseTransferRequests.RelabelItem> rows, LocalDateTime now) {
|
||||
if (rows == null || rows.isEmpty()) return;
|
||||
for (WarehouseTransferRequests.RelabelItem source : rows) {
|
||||
for (String mark : source.customerCartonMarks()) {
|
||||
WarehouseTransferRelabelBinding row = new WarehouseTransferRelabelBinding();
|
||||
row.setId(IdWorker.getId()); row.setTransferOrderId(transferId); row.setCustomerId(customerId);
|
||||
row.setOperationType(source.operationType().trim().toUpperCase());
|
||||
row.setCustomerCartonMark(mark.trim()); row.setOriginalSku(source.originalSku().trim());
|
||||
row.setNewSku(trim(source.newSku())); row.setQuantity(source.quantity());
|
||||
row.setFbaCartonMark(trim(source.fbaCartonMark()));
|
||||
row.setDestinationWarehouseCode(trim(source.destinationWarehouseCode()));
|
||||
row.setRemark(trim(source.remark())); row.setCreatedAt(now); relabelBindings.insert(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RelabelResponses.Detail createLinkedRelabel(String customerId, String operatorId,
|
||||
WarehouseTransferOrder transfer,
|
||||
Map<Long, WarehouseTransferItem> itemById,
|
||||
Map<String, List<ConfirmedStockSource>> stockSourcesBySku) {
|
||||
Map<String, WarehouseTransferItem> itemsBySku = itemById.values().stream()
|
||||
.collect(Collectors.toMap(WarehouseTransferItem::getSkuCode, Function.identity(), (left, right) -> left));
|
||||
List<RelabelRequests.Item> items = new ArrayList<>();
|
||||
for (WarehouseTransferRelabelBinding binding : relabelBindings.listOrder(customerId, transfer.getId())) {
|
||||
WarehouseTransferItem source = itemsBySku.get(binding.getOriginalSku());
|
||||
ConfirmedStockSource stockSource = nextStockSource(stockSourcesBySku.get(binding.getOriginalSku()));
|
||||
if (source == null || stockSource == null) {
|
||||
throw conflict("Relabel binding cannot find confirmed inventory for SKU " + binding.getOriginalSku());
|
||||
}
|
||||
List<RelabelRequests.SkuItem> skuItems = Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(binding.getOperationType())
|
||||
? List.of(new RelabelRequests.SkuItem(binding.getOriginalSku(), binding.getNewSku(),
|
||||
binding.getQuantity(), binding.getRemark())) : List.of();
|
||||
items.add(new RelabelRequests.Item(stockSource.balanceId, stockSource.positionId, transfer.getInboundOrderId(),
|
||||
binding.getOriginalSku(), null, null, null, source.getProductNameZh(), 1, 1,
|
||||
StringUtils.hasText(transfer.getFbaShipmentNo()) ? transfer.getFbaShipmentNo() : transfer.getTransferNo(),
|
||||
transfer.getTrackingNo(), binding.getNewSku(), "仓储转运自动换标", binding.getRemark(),
|
||||
null, null, null, binding.getCustomerCartonMark(), binding.getFbaCartonMark(),
|
||||
binding.getDestinationWarehouseCode(), binding.getOperationType(), skuItems));
|
||||
}
|
||||
RelabelRequests.Create request = new RelabelRequests.Create(transfer.getTransferNo(),
|
||||
transfer.getOperatingWarehouseId(), transfer.getLabelDeadline(), transfer.getRelabelContactName(),
|
||||
transfer.getRelabelContactPhone(), transfer.getRelabelChargeAmount(), "仓储转运确认后自动创建换标单",
|
||||
"WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL", items,
|
||||
trim(transfer.getRelabelDeliveryMethod()), false);
|
||||
return relabelService.create(customerId, operatorId, request);
|
||||
}
|
||||
|
||||
private static ConfirmedStockSource nextStockSource(List<ConfirmedStockSource> sources) {
|
||||
if (sources == null) return null;
|
||||
for (ConfirmedStockSource source : sources) {
|
||||
if (source.remainingPackages > 0) {
|
||||
source.remainingPackages--;
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class ConfirmedStockSource {
|
||||
private final Long balanceId;
|
||||
private final Long positionId;
|
||||
private int remainingPackages;
|
||||
|
||||
private ConfirmedStockSource(Long balanceId, Long positionId, int remainingPackages) {
|
||||
this.balanceId = balanceId;
|
||||
this.positionId = positionId;
|
||||
this.remainingPackages = remainingPackages;
|
||||
}
|
||||
}
|
||||
|
||||
private void createDispatchPlan(String customerId, WarehouseTransferOrder transfer,
|
||||
List<WarehouseTransferItem> items, List<WarehouseTransferPallet> pallets,
|
||||
List<WarehouseTransferPalletItem> palletItems, LocalDateTime now) {
|
||||
long planId = IdWorker.getId();
|
||||
InboundPlan plan = new InboundPlan(); plan.setId(planId); plan.setPlanNo("WT-" + transfer.getTransferNo());
|
||||
plan.setCustomerId(customerId); plan.setSourceOrderId(transfer.getLastlegOrderId());
|
||||
plan.setContainerNo(transfer.getTransferNo()); plan.setOverseasWarehouseId(Long.parseLong(transfer.getOperatingWarehouseId()));
|
||||
plan.setPlannedArrivalDate(transfer.getEstimatedArrivalAt().toLocalDate()); plan.setPalletType("STANDARD");
|
||||
plan.setAssignedFlag(true); plan.setMergeRequired(false); plan.setUrgentFlag(false);
|
||||
plan.setCreationMode("WAREHOUSE_TRANSFER"); plan.setPlanRemark("Customer-confirmed transfer measurements");
|
||||
plan.setStatus("COMPLETED"); plan.setLineCount(items.size()); plan.setPalletCount(pallets.size());
|
||||
plan.setTotalVolumeCbm(pallets.stream().map(WarehouseTransferPallet::getVolumeCbm)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)); plan.setCreatedAt(now); plan.setUpdatedAt(now); plan.setVersion(0);
|
||||
inboundPlans.insert(plan);
|
||||
Map<Long, Long> lineIds = new java.util.HashMap<>();
|
||||
for (WarehouseTransferItem source : items) {
|
||||
InboundPlanLine line = new InboundPlanLine(); line.setId(IdWorker.getId()); line.setCustomerId(customerId);
|
||||
line.setPlanId(planId); line.setOrderId(transfer.getLastlegOrderId()); line.setInboundCode(transfer.getTransferNo());
|
||||
line.setDestinationType("FBA"); line.setFbaWarehouseCode(transfer.getFbaWarehouseCode());
|
||||
line.setDestinationWarehouseCode(transfer.getFbaWarehouseCode()); line.setDestinationWarehouseName(transfer.getFbaWarehouseCode());
|
||||
line.setVolumeCbm(BigDecimal.ZERO); line.setRequiredPackageCount(source.getPackageCount());
|
||||
line.setPlannedPalletCount((int) palletItems.stream().filter(row -> row.getTransferItemId().equals(source.getId())).count());
|
||||
inboundPlanLines.insert(line); lineIds.put(source.getId(), line.getId());
|
||||
}
|
||||
for (WarehouseTransferPallet source : pallets) {
|
||||
InboundPlanPallet pallet = new InboundPlanPallet(); pallet.setId(IdWorker.getId()); pallet.setCustomerId(customerId);
|
||||
pallet.setPlanId(planId); pallet.setPalletNo(source.getPalletNo()); pallet.setPalletType("STANDARD");
|
||||
pallet.setDestinationWarehouseCode(transfer.getFbaWarehouseCode()); pallet.setDestinationWarehouseName(transfer.getFbaWarehouseCode());
|
||||
pallet.setAllocatedVolumeCbm(source.getVolumeCbm()); pallet.setSequenceNo(source.getSequenceNo());
|
||||
pallet.setWarehousePalletId(source.getWarehousePalletId()); pallet.setWarehouseLocationId(source.getWarehouseLocationId());
|
||||
pallet.setWarehouseResourceLockId(source.getWarehouseResourceLockId()); pallet.setAssignmentStatus("ASSIGNED");
|
||||
pallet.setLockedAt(source.getCompletedAt()); pallet.setActualVolumeCbm(source.getVolumeCbm());
|
||||
pallet.setActualPackageCount(source.getCartonCount()); inboundPlanPallets.insert(pallet);
|
||||
for (WarehouseTransferPalletItem sourceItem : palletItems.stream()
|
||||
.filter(row -> row.getTransferPalletId().equals(source.getId())).toList()) {
|
||||
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation(); allocation.setId(IdWorker.getId());
|
||||
allocation.setCustomerId(customerId); allocation.setPlanId(planId); allocation.setLineId(lineIds.get(sourceItem.getTransferItemId()));
|
||||
allocation.setPalletId(pallet.getId()); allocation.setAllocatedVolumeCbm(BigDecimal.ZERO);
|
||||
allocation.setPlannedPackageCount(sourceItem.getPackageCount()); allocation.setCountedPackageCount(sourceItem.getPackageCount());
|
||||
allocation.setPickedPackageCount(0); allocation.setActualVolumeCbm(source.getVolumeCbm());
|
||||
allocation.setStatus("COMPLETED"); allocation.setVersion(0); inboundPlanAllocations.insert(allocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> splitMarks(String value) {
|
||||
if (!StringUtils.hasText(value)) return List.of();
|
||||
return java.util.Arrays.stream(value.split("[,\\n\\r]+"))
|
||||
.map(String::trim).filter(StringUtils::hasText).distinct().toList();
|
||||
}
|
||||
|
||||
private static String escape(String value) {
|
||||
return org.springframework.web.util.HtmlUtils.htmlEscape(Objects.toString(value, ""));
|
||||
}
|
||||
|
||||
private static String text(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
private Long createInboundSnapshot(String customerId, long inboundOrderId, String transferNo,
|
||||
WarehouseTransferRequests.Create request, String requestHash,
|
||||
LocalDateTime now) {
|
||||
@@ -544,10 +1000,11 @@ public class WarehouseTransferService {
|
||||
|
||||
private void insertTransferCarton(String customerId, long transferId, long transferItemId,
|
||||
String transferNo, int cartonIndex, int totalCartons,
|
||||
LocalDateTime now) {
|
||||
String customerCartonMark, 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.setCustomerCartonMark(trim(customerCartonMark));
|
||||
carton.setCartonIndex(cartonIndex); carton.setTotalCartons(totalCartons); carton.setStatus("EXPECTED");
|
||||
carton.setCreatedAt(now); carton.setUpdatedAt(now); transferCartons.insert(carton);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user