修改提交
This commit is contained in:
+21
-8
@@ -7,6 +7,8 @@ import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collections;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -28,7 +30,19 @@ public final class DeliveryRequests {
|
||||
@Size(max = 128) String appointmentAccount,
|
||||
@Size(max = 128) String sellerName,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotNull @Valid @Size(min = 1, max = 20) List<FeeItem> fees) {}
|
||||
@NotNull @Valid @Size(min = 1, max = 20) List<FeeItem> fees,
|
||||
Map<String, List<@NotBlank @Size(max = 128) String>> palletCartonMarks) {
|
||||
public Transfer(String idempotencyKey, LocalDateTime plannedDeliveryAt, String deliveryMethod, String waybillNo,
|
||||
String carrierName, String pickupMethod, String loadingType, String vehicleType, String vehicleSize,
|
||||
String driverName, String driverPhone, String vehiclePlate, String appointmentAccount,
|
||||
String sellerName, String remark, List<FeeItem> fees) {
|
||||
this(idempotencyKey, plannedDeliveryAt, deliveryMethod, waybillNo, carrierName, pickupMethod, loadingType,
|
||||
vehicleType, vehicleSize, driverName, driverPhone, vehiclePlate, appointmentAccount, sellerName, remark, fees, Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
public record PalletCartonSelection(@NotBlank @Size(max = 128) String palletNo,
|
||||
@NotNull @Size(min = 1, max = 500) List<@NotBlank @Size(max = 128) String> cartonMarks) {}
|
||||
|
||||
public record FeeItem(@NotBlank @Size(max = 64) String feeType,
|
||||
@NotBlank @Size(max = 8) String currency,
|
||||
@@ -41,12 +55,6 @@ public final class DeliveryRequests {
|
||||
String driverName, String deliveryMethod,
|
||||
LocalDateTime plannedFrom, LocalDateTime plannedTo) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
public Query(long page, long size, String documentNo, String warehouseId, String status,
|
||||
String dispatchNo, String waybillNo, String vehiclePlate, String driverName,
|
||||
String deliveryMethod, LocalDateTime plannedFrom, LocalDateTime plannedTo) {
|
||||
this(page, size, documentNo, null, warehouseId, status, dispatchNo, waybillNo, vehiclePlate,
|
||||
driverName, deliveryMethod, plannedFrom, plannedTo);
|
||||
}
|
||||
}
|
||||
|
||||
public record PalletQuery(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
@@ -59,7 +67,12 @@ public final class DeliveryRequests {
|
||||
@Size(max=1000) String remark,
|
||||
@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record OneClickItem(@NotNull Long inventoryPositionId, @NotNull @Min(1) Long packages,
|
||||
@NotBlank @Size(max=128) String sku) {}
|
||||
@NotBlank @Size(max=128) String sku,
|
||||
@Size(max=200) List<@NotBlank @Size(max=128) String> cartonMarks) {
|
||||
public OneClickItem(Long inventoryPositionId, Long packages, String sku) {
|
||||
this(inventoryPositionId, packages, sku, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
public record PdaPick(@NotNull Long outboundPalletItemId, @NotNull Long inventoryPositionId,
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
public record PdaComplete(@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
|
||||
+3
-1
@@ -40,7 +40,9 @@ public final class DeliveryResponses {
|
||||
public record DeliveryDetail(DeliveryRow order, List<DeliverySku> skus,
|
||||
List<DeliveryFeeRow> fees, List<DeliveryOperationRow> operations) {}
|
||||
|
||||
public record OutboundDetail(OutboundRow outbound, DeliveryRow delivery) {}
|
||||
public record OutboundPalletRow(Long id, String palletNo, String cartonMarks, String skuCode, String inboundCode,
|
||||
Integer packageCount, Integer pickedPackageCount, String status) {}
|
||||
public record OutboundDetail(OutboundRow outbound, DeliveryRow delivery, List<OutboundPalletRow> pallets) {}
|
||||
public record PdaTask(Long id, String taskNo, String status, String outboundNo, LocalDateTime pushedAt, LocalDateTime completedAt) {}
|
||||
public record PdaPickPosition(Long inventoryPositionId, String palletNo, String cartonMark,
|
||||
String skuCode, String inboundCode, long availablePackages,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ public class OutboundPalletItem {
|
||||
private Integer pickedPackageCount;
|
||||
private String status;
|
||||
private String skuCode;
|
||||
private String cartonMarks;
|
||||
private String orderRefs;
|
||||
private String inboundCode;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
+29
-3
@@ -26,6 +26,7 @@ import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPdaTask;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPdaPick;
|
||||
@@ -78,6 +79,7 @@ public class DeliveryTransferService {
|
||||
@Resource private DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@Resource private InboundPlanPalletAllocationMapper planAllocations;
|
||||
@Resource private InboundPlanLineMapper planLines;
|
||||
@Resource private InboundPlanCartonMapper inboundCartons;
|
||||
@Resource private LastlegOrderMapper tailOrders;
|
||||
@Resource private LastlegOrderStatusHistoryMapper histories;
|
||||
@Resource private LastlegOrderDetailMapper tailOrderDetails;
|
||||
@@ -156,6 +158,15 @@ 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()));
|
||||
} 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(","));
|
||||
item.setCartonMarks(StringUtils.hasText(marks) ? marks : null);
|
||||
}
|
||||
item.setOrderRefs(source.getOrderRefs()); item.setInboundCode(source.getInboundCode()); item.setCreatedAt(now);
|
||||
outboundPallets.insert(item);
|
||||
}
|
||||
@@ -230,7 +241,7 @@ public class DeliveryTransferService {
|
||||
InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId());
|
||||
if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found");
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId());
|
||||
item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo()); item.setPackageCount(input.packages().intValue()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); item.setSkuCode(input.sku().trim()); item.setCreatedAt(now);
|
||||
item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo()); item.setPackageCount(input.packages().intValue()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); item.setSkuCode(input.sku().trim()); item.setCartonMarks(String.join(",", input.cartonMarks() == null ? List.of() : input.cartonMarks())); item.setCreatedAt(now);
|
||||
outboundPallets.insert(item);
|
||||
}
|
||||
outbound.setOverseasWarehouseId(selectedWarehouseId); outboundOrders.updateById(outbound);
|
||||
@@ -258,6 +269,15 @@ public class DeliveryTransferService {
|
||||
String key = profile.getId() + "\u0000" + input.sku().trim();
|
||||
requestedPackages.merge(key, input.packages(), Long::sum);
|
||||
profiles.put(key, profile);
|
||||
if (input.cartonMarks() != null && !input.cartonMarks().isEmpty()) {
|
||||
if (input.cartonMarks().size() != input.packages() || new HashSet<>(input.cartonMarks()).size() != input.cartonMarks().size()) {
|
||||
throw conflict("Specified carton marks must be unique and equal the package count");
|
||||
}
|
||||
List<DropshipCarton> selected = dropshipCartons.listProfile(customerId, profile.getId()).stream()
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && input.sku().trim().equals(carton.getSkuCode())
|
||||
&& input.cartonMarks().contains(carton.getCartonMark())).toList();
|
||||
if (selected.size() != input.cartonMarks().size()) throw conflict("Specified carton marks are unavailable");
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, Long> requested : requestedPackages.entrySet()) {
|
||||
String sku = requested.getKey().substring(requested.getKey().indexOf('\u0000') + 1);
|
||||
@@ -305,7 +325,11 @@ public class DeliveryTransferService {
|
||||
}
|
||||
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, request.inventoryPositionId());
|
||||
if (profile == null) throw conflict("Scanned pallet is not one-click dropship inventory");
|
||||
List<DropshipCarton> cartons = dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
|
||||
List<DropshipCarton> cartons = StringUtils.hasText(item.getCartonMarks())
|
||||
? Arrays.stream(item.getCartonMarks().split(",")).map(String::trim).filter(StringUtils::hasText)
|
||||
.map(mark -> dropshipCartons.findByProfileAndMark(customerId, profile.getId(), mark)).filter(Objects::nonNull)
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && item.getSkuCode().equals(carton.getSkuCode())).toList()
|
||||
: dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
|
||||
if (cartons.size() != request.packages()) throw conflict("Insufficient available one-click dropship cartons");
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":ALLOCATE", null);
|
||||
inventoryCommands.allocate(customerId, operatorId, move);
|
||||
@@ -481,7 +505,9 @@ public class DeliveryTransferService {
|
||||
if (outbound == null) throw notFound("Outbound order not found");
|
||||
String customerId = outbound.getCustomerId();
|
||||
DeliveryOrder delivery = requireDelivery(customerId, outbound.getId());
|
||||
return new DeliveryResponses.OutboundDetail(outboundRow(outbound), deliveryRow(delivery));
|
||||
List<DeliveryResponses.OutboundPalletRow> pallets = outboundPallets.findByOutbound(customerId, outbound.getId()).stream()
|
||||
.map(item -> new DeliveryResponses.OutboundPalletRow(item.getId(), item.getPalletNo(), item.getCartonMarks(), item.getSkuCode(), item.getInboundCode(), item.getPackageCount(), item.getPickedPackageCount(), item.getStatus())).toList();
|
||||
return new DeliveryResponses.OutboundDetail(outboundRow(outbound), deliveryRow(delivery), pallets);
|
||||
}
|
||||
|
||||
public PageResult<DeliveryResponses.DeliveryRow> deliveryPage(String customerId, DeliveryRequests.Query query) {
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ import java.util.List;
|
||||
public interface DropshipCartonMapper extends BaseMapper<DropshipCarton> {
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND carton_mark=#{cartonMark} LIMIT 1")
|
||||
DropshipCarton find(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("cartonMark") String cartonMark);
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} AND carton_mark=#{cartonMark} LIMIT 1 FOR UPDATE")
|
||||
DropshipCarton findByProfileAndMark(@Param("customerId") String customerId, @Param("profileId") Long profileId, @Param("cartonMark") String cartonMark);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} ORDER BY carton_sequence")
|
||||
List<DropshipCarton> listProfile(@Param("customerId") String customerId, @Param("profileId") Long profileId);
|
||||
|
||||
-9
@@ -28,15 +28,6 @@ public final class InboundPlanRequests {
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo
|
||||
) {
|
||||
public Query(long page, long size, String planNo, String inboundCode, String status,
|
||||
String overseasWarehouseId, String destinationWarehouseCode, String warehouse,
|
||||
String fbaWarehouseCode, String appointmentStatus, String dispatchStatus,
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
this(page, size, planNo, inboundCode, null, status, overseasWarehouseId,
|
||||
destinationWarehouseCode, warehouse, fbaWarehouseCode, appointmentStatus,
|
||||
dispatchStatus, createdFrom, createdTo);
|
||||
}
|
||||
|
||||
public Query {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
|
||||
+2
@@ -10,6 +10,8 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPlanCartonMapper extends BaseMapper<InboundPlanCarton> {
|
||||
@Select("SELECT * FROM ll_inbound_plan_carton WHERE customer_id=#{customerId} AND (warehouse_pallet_id=#{palletId} OR plan_pallet_id=#{palletId}) ORDER BY id")
|
||||
List<InboundPlanCarton> listByPallet(@Param("customerId") String customerId, @Param("palletId") Long palletId);
|
||||
@Select("""
|
||||
SELECT l.* FROM ll_inbound_plan_line l
|
||||
JOIN ll_order_detail d ON d.order_id=l.order_id
|
||||
|
||||
+4
@@ -16,6 +16,10 @@ public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> {
|
||||
List<InventoryPosition> listBalance(@Param("customerId") String customerId,
|
||||
@Param("balanceId") Long balanceId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE id=#{positionId} AND customer_id=#{customerId} LIMIT 1")
|
||||
InventoryPosition findTenantPosition(@Param("customerId") String customerId,
|
||||
@Param("positionId") Long positionId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND pallet_id=#{palletId} " +
|
||||
"ORDER BY balance_id,batch_no")
|
||||
List<InventoryPosition> listPallet(@Param("customerId") String customerId,
|
||||
|
||||
+8
@@ -12,11 +12,19 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/relabel-applications")
|
||||
public class MerchantRelabelApplicationController {
|
||||
@Resource private RelabelApplicationService service;
|
||||
@GetMapping("/inventory-candidates")
|
||||
public ApiResponse<List<RelabelResponses.InventoryCandidate>> candidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String operationWarehouseId,
|
||||
@RequestParam(required = false) String inboundCode) {
|
||||
return ApiResponse.success(service.candidates(customer(user), operationWarehouseId, inboundCode));
|
||||
}
|
||||
@PostMapping
|
||||
public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody RelabelRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||
|
||||
@@ -19,7 +19,7 @@ public final class RelabelRequests {
|
||||
|
||||
public record Create(
|
||||
@NotBlank @Size(max = 128) String customerOrderNo,
|
||||
@Size(max = 128) String overseasWarehouseId,
|
||||
@NotBlank @Size(max = 128) String operationWarehouseId,
|
||||
@NotNull LocalDateTime labelDeadline,
|
||||
@NotBlank @Size(max = 128) String contactName,
|
||||
@NotBlank @Size(max = 64) String contactPhone,
|
||||
@@ -30,6 +30,9 @@ public final class RelabelRequests {
|
||||
) {}
|
||||
|
||||
public record Item(
|
||||
@NotNull Long sourceInventoryBalanceId,
|
||||
@NotNull Long sourceInventoryPositionId,
|
||||
@NotNull Long sourceInboundOrderId,
|
||||
@Size(max = 128) String skuCode,
|
||||
@Size(max = 128) String productImageFileId,
|
||||
@Size(max = 255) String productImageFileName,
|
||||
@@ -39,26 +42,61 @@ public final class RelabelRequests {
|
||||
@Positive int notifiedPackageCount,
|
||||
@NotBlank @Size(max = 128) String shipmentNo,
|
||||
@Size(max = 255) String trackingCode,
|
||||
@NotBlank @Size(max = 128) String newSkuCode,
|
||||
@Size(max = 128) String newSkuCode,
|
||||
@Size(max = 1000) String labelRequirement,
|
||||
@Size(max = 1000) String remark,
|
||||
@Size(max = 128) String relabelFileId,
|
||||
@Size(max = 255) String relabelFileName,
|
||||
@Size(max = 1000) String relabelFileUrl,
|
||||
@NotBlank @Size(max = 255) String cartonMark,
|
||||
@NotBlank @Size(max = 255) String fbaCartonMark,
|
||||
@Size(max = 128) String destinationWarehouseId
|
||||
@Size(max = 255) String fbaCartonMark,
|
||||
@Size(max = 128) String destinationFbaWarehouseCode,
|
||||
@Size(max = 32) String operationType,
|
||||
@Size(max = 100) List<@Valid SkuItem> skuItems
|
||||
) {
|
||||
/** 保持既有箱唛换标调用方的二进制参数形态。 */
|
||||
public Item(Long sourceInventoryBalanceId, Long sourceInventoryPositionId, Long sourceInboundOrderId,
|
||||
String skuCode, String productImageFileId, String productImageFileName, String productImageUrl,
|
||||
String productNameZh, int totalPackageCount, int notifiedPackageCount, String shipmentNo,
|
||||
String trackingCode, String newSkuCode, String labelRequirement, String remark,
|
||||
String relabelFileId, String relabelFileName, String relabelFileUrl, String cartonMark,
|
||||
String fbaCartonMark, String destinationFbaWarehouseCode) {
|
||||
this(sourceInventoryBalanceId, sourceInventoryPositionId, sourceInboundOrderId, skuCode,
|
||||
productImageFileId, productImageFileName, productImageUrl, productNameZh, totalPackageCount,
|
||||
notifiedPackageCount, shipmentNo, trackingCode, newSkuCode, labelRequirement, remark,
|
||||
relabelFileId, relabelFileName, relabelFileUrl, cartonMark, fbaCartonMark,
|
||||
destinationFbaWarehouseCode, "CARTON_LABEL", List.of());
|
||||
}
|
||||
public Item(String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode, String newSkuCode,
|
||||
String labelRequirement, String remark, String relabelFileId, String relabelFileName,
|
||||
String relabelFileUrl, String cartonMark, String fbaCartonMark,
|
||||
String destinationFbaWarehouseCode) {
|
||||
this(101L, 201L, 301L, skuCode, productImageFileId, productImageFileName, productImageUrl,
|
||||
productNameZh, totalPackageCount, notifiedPackageCount, shipmentNo, trackingCode,
|
||||
newSkuCode, labelRequirement, remark, relabelFileId, relabelFileName, relabelFileUrl,
|
||||
cartonMark, fbaCartonMark, destinationFbaWarehouseCode, "CARTON_LABEL", List.of());
|
||||
}
|
||||
public Item(String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode, String newSkuCode,
|
||||
String labelRequirement, String relabelFileId, String relabelFileName, String relabelFileUrl) {
|
||||
this(skuCode, productImageFileId, productImageFileName, productImageUrl, productNameZh,
|
||||
this(101L, 201L, 301L, skuCode, productImageFileId, productImageFileName, productImageUrl, productNameZh,
|
||||
totalPackageCount, notifiedPackageCount, shipmentNo, trackingCode, newSkuCode,
|
||||
labelRequirement, null, relabelFileId, relabelFileName, relabelFileUrl, skuCode, shipmentNo, null);
|
||||
labelRequirement, null, relabelFileId, relabelFileName, relabelFileUrl, skuCode, shipmentNo, "ONT8",
|
||||
"CARTON_LABEL", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一我方箱唛内可同时更换的多个 SKU;数量以件为单位。 */
|
||||
public record SkuItem(
|
||||
@NotBlank @Size(max = 128) String skuCode,
|
||||
@NotBlank @Size(max = 128) String newSkuCode,
|
||||
@Positive int quantity,
|
||||
@Size(max = 1000) String remark
|
||||
) {}
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String relabelNo,
|
||||
String customerOrderNo, String skuCode, String status,
|
||||
String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
|
||||
@@ -19,7 +19,19 @@ public final class RelabelResponses {
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode,
|
||||
String newSkuCode, String labelRequirement, String remark, String relabelFileId,
|
||||
String relabelFileName, String relabelFileUrl,
|
||||
String cartonMark, String fbaCartonMark, String destinationWarehouseId) {}
|
||||
String cartonMark, String fbaCartonMark, String destinationFbaWarehouseCode,
|
||||
String operationType, List<SkuItem> skuItems,
|
||||
Long sourceInventoryBalanceId, Long sourceInventoryPositionId, Long sourceInboundOrderId,
|
||||
String sourceInboundOrderNo, String sourceInboundCode, String sourceWarehouseId,
|
||||
String sourceWarehouseCode, String sourceWarehouseName) {}
|
||||
public record SkuItem(String skuCode, String newSkuCode, int quantity, String remark) {}
|
||||
public record InventoryPosition(Long positionId, String palletNo, String batchNo,
|
||||
String warehouseLocationCode, long availablePackages) {}
|
||||
public record InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo,
|
||||
String inboundCode, String overseasWarehouseId, String overseasWarehouseCode,
|
||||
String overseasWarehouseName, String destinationFbaWarehouseCode,
|
||||
String destinationFbaWarehouseName, String skuCode, String productNameZh,
|
||||
long availablePackages, long version, List<InventoryPosition> positions) {}
|
||||
public record TailOrder(Long id, String orderNo, String overseasWarehouseId, int packageCount,
|
||||
String status) {}
|
||||
public record History(String fromStatus, String toStatus, String operatorId,
|
||||
|
||||
@@ -31,5 +31,16 @@ public class RelabelItem {
|
||||
private String cartonMark;
|
||||
private String fbaCartonMark;
|
||||
private String destinationWarehouseId;
|
||||
private String destinationFbaWarehouseCode;
|
||||
private String operationType;
|
||||
private String skuRelabelItemsJson;
|
||||
private Long sourceInventoryBalanceId;
|
||||
private Long sourceInventoryPositionId;
|
||||
private Long sourceInboundOrderId;
|
||||
private String sourceInboundOrderNo;
|
||||
private String sourceInboundCode;
|
||||
private String sourceWarehouseId;
|
||||
private String sourceWarehouseCode;
|
||||
private String sourceWarehouseName;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ public final class PdaRelabelRequests {
|
||||
|
||||
public record Claim(@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record Scan(@NotNull @Positive Long itemId,
|
||||
public record Scan(@Positive Long itemId,
|
||||
@NotBlank @Size(max = 255) String oldLabel,
|
||||
@NotBlank @Size(max = 255) String newLabel,
|
||||
@Size(max = 255) String newLabel,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
|
||||
+3
@@ -23,8 +23,11 @@ public final class PdaRelabelResponses {
|
||||
public record Item(Long itemId, String skuCode, String productNameZh, String shipmentNo,
|
||||
String trackingCode, String newSkuCode, String labelRequirement,
|
||||
String relabelFileId, String relabelFileName, String relabelFileUrl,
|
||||
String cartonMark, String fbaCartonMark, String operationType,
|
||||
List<SkuItem> skuItems,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages) { }
|
||||
public record SkuItem(String skuCode, String newSkuCode, int quantity, String remark) { }
|
||||
|
||||
public record UnitFact(Long factId, Long itemId, String oldLabel, String newLabel,
|
||||
String result, String exceptionCode, String reason,
|
||||
|
||||
+38
-5
@@ -135,13 +135,23 @@ public class PdaRelabelService {
|
||||
@Transactional
|
||||
public PdaRelabelResponses.Detail scan(String operatorKey, String operatorName, long overseasWarehouseId,
|
||||
String relabelNo, PdaRelabelRequests.Scan request) {
|
||||
Long itemId = request.itemId();
|
||||
if (itemId == null) {
|
||||
RelabelApplication application = requireApplication(relabelNo, overseasWarehouseId, false);
|
||||
itemId = requireItemByCartonMark(application, request.oldLabel()).getId();
|
||||
}
|
||||
return unitAction(operatorKey, operatorName, overseasWarehouseId, relabelNo, "RELABEL", request.idempotencyKey(),
|
||||
request, request.itemId(), request.oldLabel(), request.newLabel(), "RELABELLED", null, null,
|
||||
request, itemId, request.oldLabel(), request.newLabel(), "RELABELLED", null, null,
|
||||
request.expectedVersion(), item -> {
|
||||
if (!item.getNewSkuCode().equalsIgnoreCase(request.newLabel().trim())) {
|
||||
throw conflict("Scanned new label does not match the approved new SKU");
|
||||
if (StringUtils.hasText(item.getCartonMark()) && !item.getCartonMark().trim().equalsIgnoreCase(request.oldLabel().trim())) {
|
||||
throw conflict("Scanned old carton mark does not match the relabel item");
|
||||
}
|
||||
if (request.oldLabel().trim().equalsIgnoreCase(request.newLabel().trim())) {
|
||||
String expectedFbaLabel = trim(item.getFbaCartonMark());
|
||||
if (StringUtils.hasText(request.newLabel()) && StringUtils.hasText(expectedFbaLabel)
|
||||
&& !expectedFbaLabel.equalsIgnoreCase(request.newLabel().trim())) {
|
||||
throw conflict("Scanned new label does not match the approved FBA carton mark");
|
||||
}
|
||||
if (StringUtils.hasText(expectedFbaLabel) && request.oldLabel().trim().equalsIgnoreCase(expectedFbaLabel)) {
|
||||
throw conflict("Old and new labels must be different");
|
||||
}
|
||||
});
|
||||
@@ -277,7 +287,11 @@ public class PdaRelabelService {
|
||||
PdaRelabelUnitFact fact = new PdaRelabelUnitFact();
|
||||
fact.setId(IdWorker.getId()); fact.setTaskId(task.getId()); fact.setApplicationId(application.getId());
|
||||
fact.setItemId(itemId); fact.setCustomerId(application.getCustomerId());
|
||||
fact.setOldLabel(normalizedOldLabel); fact.setNewLabel(trim(newLabel)); fact.setResult(resultCode);
|
||||
String resolvedNewLabel = trim(newLabel);
|
||||
if ("RELABELLED".equals(resultCode) && !StringUtils.hasText(resolvedNewLabel)) {
|
||||
resolvedNewLabel = trim(item.getFbaCartonMark());
|
||||
}
|
||||
fact.setOldLabel(normalizedOldLabel); fact.setNewLabel(resolvedNewLabel); fact.setResult(resultCode);
|
||||
fact.setExceptionCode(exceptionCode); fact.setReason(trim(reason));
|
||||
fact.setOperatorKey(operatorKey); fact.setOperatorName(operatorName); fact.setOccurredAt(LocalDateTime.now());
|
||||
try { unitFacts.insert(fact); }
|
||||
@@ -328,6 +342,14 @@ public class PdaRelabelService {
|
||||
if (item == null) throw notFound("Relabel item does not belong to the current application");
|
||||
return item;
|
||||
}
|
||||
private RelabelItem requireItemByCartonMark(RelabelApplication application, String cartonMark) {
|
||||
if (!StringUtils.hasText(cartonMark)) throw badRequest("Carton mark is required when itemId is omitted");
|
||||
List<RelabelItem> matches = items.selectList(new LambdaQueryWrapper<RelabelItem>()
|
||||
.eq(RelabelItem::getApplicationId, application.getId()).eq(RelabelItem::getCustomerId, application.getCustomerId())
|
||||
.eq(RelabelItem::getCartonMark, cartonMark.trim()));
|
||||
if (matches.size() != 1) throw notFound("Scanned carton mark does not belong to the current relabel application");
|
||||
return matches.get(0);
|
||||
}
|
||||
|
||||
private void requireVersion(PdaRelabelTask task, Integer expectedVersion) {
|
||||
if (!Objects.equals(task.getVersion(), expectedVersion)) {
|
||||
@@ -380,6 +402,7 @@ public class PdaRelabelService {
|
||||
return new PdaRelabelResponses.Item(item.getId(), item.getSkuCode(), item.getProductNameZh(),
|
||||
item.getShipmentNo(), item.getTrackingCode(), item.getNewSkuCode(), item.getLabelRequirement(),
|
||||
item.getRelabelFileId(), item.getRelabelFileName(), item.getRelabelFileUrl(),
|
||||
item.getCartonMark(), item.getFbaCartonMark(), operationType(item), readSkuItems(item.getSkuRelabelItemsJson()),
|
||||
value(item.getNotifiedPackageCount()), count(itemFacts, "RELABELLED"), count(itemFacts, "SKIPPED"),
|
||||
count(itemFacts, "EXCEPTION"));
|
||||
}).toList();
|
||||
@@ -462,6 +485,16 @@ public class PdaRelabelService {
|
||||
return objectMapper.readValue(value, objectMapper.getTypeFactory().constructCollectionType(List.class, Long.class));
|
||||
} catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read relabel photo IDs", exception); }
|
||||
}
|
||||
private List<PdaRelabelResponses.SkuItem> readSkuItems(String value) {
|
||||
if (!StringUtils.hasText(value)) return List.of();
|
||||
try {
|
||||
return objectMapper.readValue(value, objectMapper.getTypeFactory().constructCollectionType(
|
||||
List.class, PdaRelabelResponses.SkuItem.class));
|
||||
} catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read relabel SKU details", exception); }
|
||||
}
|
||||
private static String operationType(RelabelItem item) {
|
||||
return StringUtils.hasText(item.getOperationType()) ? item.getOperationType() : "CARTON_LABEL";
|
||||
}
|
||||
|
||||
private static int count(List<PdaRelabelUnitFact> facts, String result) {
|
||||
return (int) facts.stream().filter(fact -> result.equals(fact.getResult())).count();
|
||||
|
||||
+169
-43
@@ -8,6 +8,12 @@ import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
@@ -38,6 +44,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -53,35 +60,90 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private WalletCommandOutboxService walletCommands;
|
||||
@Resource private InventoryCommandService inventoryCommands;
|
||||
@Resource private InventoryBalanceMapper inventoryBalances;
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<RelabelResponses.InventoryCandidate> candidates(String customerId, String operationWarehouseId,
|
||||
String inboundCode) {
|
||||
if (!StringUtils.hasText(operationWarehouseId)) throw badRequest("Operation warehouse is required");
|
||||
List<InventoryBalance> balances = inventoryBalances.selectList(new LambdaQueryWrapper<InventoryBalance>()
|
||||
.eq(InventoryBalance::getCustomerId, customerId)
|
||||
.eq(InventoryBalance::getOverseasWarehouseId, operationWarehouseId.trim())
|
||||
.gt(InventoryBalance::getAvailablePackages, 0)
|
||||
.like(StringUtils.hasText(inboundCode), InventoryBalance::getInboundCode, trim(inboundCode))
|
||||
.orderByDesc(InventoryBalance::getUpdatedAt));
|
||||
return balances.stream().map(balance -> {
|
||||
List<RelabelResponses.InventoryPosition> positions = inventoryPositions.listBalance(customerId, balance.getId())
|
||||
.stream().filter(position -> value(position.getAvailablePackages()) > 0)
|
||||
.map(position -> new RelabelResponses.InventoryPosition(position.getId(), position.getPalletNo(),
|
||||
position.getBatchNo(), position.getWarehouseLocationCode(), value(position.getAvailablePackages())))
|
||||
.toList();
|
||||
long available = positions.stream().mapToLong(RelabelResponses.InventoryPosition::availablePackages).sum();
|
||||
return new RelabelResponses.InventoryCandidate(balance.getId(), balance.getInboundOrderId(),
|
||||
balance.getInboundOrderNo(), balance.getInboundCode(), balance.getOverseasWarehouseId(),
|
||||
balance.getOverseasWarehouseCode(), balance.getOverseasWarehouseName(),
|
||||
balance.getDestinationWarehouseCode(), balance.getDestinationWarehouseName(), balance.getSkuCode(),
|
||||
balance.getProductNameZh(), available, value(balance.getVersion()), positions);
|
||||
}).filter(candidate -> candidate.availablePackages() > 0).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
|
||||
int total = 0;
|
||||
int notified = 0;
|
||||
Map<String, Integer> packagesByWarehouse = new LinkedHashMap<>();
|
||||
for (RelabelRequests.Item item : request.items()) {
|
||||
if (item.notifiedPackageCount() > item.totalPackageCount()) {
|
||||
throw badRequest("Notified relocation packages cannot exceed total packages for SKU " + item.skuCode());
|
||||
}
|
||||
total = Math.addExact(total, item.totalPackageCount());
|
||||
notified = Math.addExact(notified, item.notifiedPackageCount());
|
||||
String warehouseId = destination(item, request.overseasWarehouseId());
|
||||
packagesByWarehouse.merge(warehouseId, item.totalPackageCount(), Math::addExact);
|
||||
}
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
String requestHash = hash(request);
|
||||
RelabelApplication replay = applications.findKey(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
if (!requestHash.equals(replay.getRequestHash())) throw conflict("Idempotency key was reused with different relabel data");
|
||||
return detail(customerId, replay.getRelabelNo());
|
||||
}
|
||||
int total = 0;
|
||||
int notified = 0;
|
||||
List<SourceInventory> sources = new java.util.ArrayList<>();
|
||||
Set<Long> selectedPositions = new HashSet<>();
|
||||
Set<String> selectedCartonMarks = new HashSet<>();
|
||||
for (RelabelRequests.Item item : request.items()) {
|
||||
validateOperation(item);
|
||||
if (item.notifiedPackageCount() > item.totalPackageCount()) {
|
||||
throw badRequest("Notified relocation packages cannot exceed total packages for SKU " + item.skuCode());
|
||||
}
|
||||
total = Math.addExact(total, item.totalPackageCount());
|
||||
notified = Math.addExact(notified, item.notifiedPackageCount());
|
||||
if (!selectedPositions.add(item.sourceInventoryPositionId())) {
|
||||
throw badRequest("The same inventory position cannot be selected more than once");
|
||||
}
|
||||
if (!selectedCartonMarks.add(item.cartonMark().trim().toUpperCase())) {
|
||||
throw badRequest("The same carton mark cannot create more than one relabel task");
|
||||
}
|
||||
InventoryPosition position = inventoryPositions.findTenantPosition(customerId, item.sourceInventoryPositionId());
|
||||
if (position == null || !Objects.equals(position.getBalanceId(), item.sourceInventoryBalanceId())) {
|
||||
throw conflict("Selected inventory position is no longer available");
|
||||
}
|
||||
InventoryBalance balance = inventoryBalances.selectById(item.sourceInventoryBalanceId());
|
||||
if (balance == null || !customerId.equals(balance.getCustomerId())
|
||||
|| !request.operationWarehouseId().trim().equals(balance.getOverseasWarehouseId())
|
||||
|| !Objects.equals(balance.getInboundOrderId(), item.sourceInboundOrderId())) {
|
||||
throw conflict("Selected inventory does not belong to this customer, operation warehouse and inbound order");
|
||||
}
|
||||
if (value(position.getAvailablePackages()) < item.totalPackageCount()) {
|
||||
throw conflict("Available inventory changed for inbound " + balance.getInboundOrderNo());
|
||||
}
|
||||
sources.add(new SourceInventory(item, balance));
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long applicationId = IdWorker.getId();
|
||||
String primaryWarehouseId = packagesByWarehouse.keySet().iterator().next();
|
||||
String relabelNo = orderNoGenerator.next("RELABEL", primaryWarehouseId, applicationId);
|
||||
List<LastlegOrder> tailOrders = packagesByWarehouse.entrySet().stream()
|
||||
.map(entry -> insertTailOrder(customerId, request, requestHash, relabelNo,
|
||||
entry.getKey(), entry.getValue(), now))
|
||||
.toList();
|
||||
String operationWarehouseId = request.operationWarehouseId().trim();
|
||||
String relabelNo = orderNoGenerator.next("RELABEL", operationWarehouseId, applicationId);
|
||||
for (SourceInventory source : sources) {
|
||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
source.request().sourceInventoryPositionId(), source.request().totalPackageCount(), relabelNo,
|
||||
null, "RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":HOLD:"
|
||||
+ source.request().sourceInventoryPositionId(), "贴标申请冻结原入仓单库存"));
|
||||
}
|
||||
LastlegOrder tailOrder = insertTailOrder(customerId, request, requestHash, relabelNo,
|
||||
operationWarehouseId, total, now);
|
||||
List<LastlegOrder> tailOrders = List.of(tailOrder);
|
||||
|
||||
RelabelApplication application = new RelabelApplication();
|
||||
application.setId(applicationId);
|
||||
@@ -89,7 +151,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setRelabelNo(relabelNo);
|
||||
application.setLastlegOrderId(tailOrders.get(0).getId());
|
||||
application.setCustomerOrderNo(request.customerOrderNo().trim());
|
||||
application.setOverseasWarehouseId(primaryWarehouseId);
|
||||
application.setOverseasWarehouseId(operationWarehouseId);
|
||||
application.setLabelDeadline(request.labelDeadline());
|
||||
application.setContactName(request.contactName().trim());
|
||||
application.setContactPhone(request.contactPhone().trim());
|
||||
@@ -113,8 +175,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setWalletCommandEventId(walletEventId);
|
||||
applications.updateById(application);
|
||||
|
||||
for (RelabelRequests.Item source : request.items()) {
|
||||
insertItem(customerId, applicationId, source, request.overseasWarehouseId(), now);
|
||||
for (SourceInventory source : sources) {
|
||||
insertItem(customerId, applicationId, source, now);
|
||||
}
|
||||
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
|
||||
tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
|
||||
@@ -132,9 +194,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
|
||||
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
|
||||
.eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status())
|
||||
.apply(StringUtils.hasText(query.overseasWarehouseId()),
|
||||
"EXISTS (SELECT 1 FROM ll_relabel_item rw WHERE rw.application_id=ll_relabel_application.id AND rw.customer_id=ll_relabel_application.customer_id AND rw.destination_warehouse_id={0})",
|
||||
query.overseasWarehouseId())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), RelabelApplication::getOverseasWarehouseId,
|
||||
trim(query.overseasWarehouseId()))
|
||||
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
|
||||
.apply(StringUtils.hasText(query.skuCode()),
|
||||
@@ -185,13 +246,17 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
if ("COMPLETED".equals(next)) {
|
||||
releaseInventory(customerId, operatorId, application);
|
||||
walletCommands.publish("SETTLE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null,
|
||||
"换标完成,结算费用", 2L);
|
||||
application.setWalletStatus("SETTLE_PENDING"); applications.updateById(application);
|
||||
} else if (Set.of("REJECTED", "CANCELLED").contains(next) && "FROZEN".equals(application.getWalletStatus())) {
|
||||
walletCommands.publish("RELEASE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null,
|
||||
"换标取消,释放冻结", 2L);
|
||||
application.setWalletStatus("RELEASE_PENDING"); applications.updateById(application);
|
||||
} else if (Set.of("REJECTED", "CANCELLED").contains(next)) {
|
||||
releaseInventory(customerId, operatorId, application);
|
||||
if ("FROZEN".equals(application.getWalletStatus())) {
|
||||
walletCommands.publish("RELEASE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, null,
|
||||
"换标取消,释放冻结", 2L);
|
||||
application.setWalletStatus("RELEASE_PENDING"); applications.updateById(application);
|
||||
}
|
||||
}
|
||||
List<LastlegOrder> linkedOrders = tailOrders(application);
|
||||
if (linkedOrders.isEmpty()) throw conflict("Linked tail-leg order is missing");
|
||||
@@ -217,30 +282,41 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
private void insertItem(String customerId, long applicationId, RelabelRequests.Item source,
|
||||
String defaultWarehouseId, LocalDateTime now) {
|
||||
private void insertItem(String customerId, long applicationId, SourceInventory sourceInventory,
|
||||
LocalDateTime now) {
|
||||
RelabelRequests.Item source = sourceInventory.request();
|
||||
InventoryBalance balance = sourceInventory.balance();
|
||||
RelabelItem item = new RelabelItem();
|
||||
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId);
|
||||
item.setSkuCode(trim(source.skuCode())); item.setProductImageFileId(trim(source.productImageFileId()));
|
||||
item.setProductImageFileName(trim(source.productImageFileName())); item.setProductImageUrl(trim(source.productImageUrl()));
|
||||
item.setProductNameZh(source.productNameZh().trim()); item.setTotalPackageCount(source.totalPackageCount());
|
||||
item.setNotifiedPackageCount(source.notifiedPackageCount()); item.setShipmentNo(source.shipmentNo().trim());
|
||||
item.setTrackingCode(trim(source.trackingCode())); item.setNewSkuCode(source.newSkuCode().trim());
|
||||
item.setTrackingCode(trim(source.trackingCode())); item.setNewSkuCode(trim(source.newSkuCode()));
|
||||
item.setLabelRequirement(trim(source.labelRequirement())); item.setRemark(trim(source.remark())); item.setRelabelFileId(trim(source.relabelFileId()));
|
||||
item.setRelabelFileName(trim(source.relabelFileName())); item.setRelabelFileUrl(trim(source.relabelFileUrl()));
|
||||
item.setCartonMark(source.cartonMark().trim()); item.setFbaCartonMark(source.fbaCartonMark().trim());
|
||||
item.setDestinationWarehouseId(destination(source, defaultWarehouseId));
|
||||
item.setCartonMark(source.cartonMark().trim()); item.setFbaCartonMark(trim(source.fbaCartonMark()));
|
||||
item.setDestinationWarehouseId(trim(source.destinationFbaWarehouseCode()));
|
||||
item.setDestinationFbaWarehouseCode(trim(source.destinationFbaWarehouseCode()));
|
||||
item.setOperationType(operationType(source));
|
||||
item.setSkuRelabelItemsJson(writeSkuItems(source.skuItems()));
|
||||
item.setSourceInventoryBalanceId(balance.getId());
|
||||
item.setSourceInventoryPositionId(source.sourceInventoryPositionId());
|
||||
item.setSourceInboundOrderId(balance.getInboundOrderId());
|
||||
item.setSourceInboundOrderNo(balance.getInboundOrderNo());
|
||||
item.setSourceInboundCode(balance.getInboundCode());
|
||||
item.setSourceWarehouseId(balance.getOverseasWarehouseId());
|
||||
item.setSourceWarehouseCode(balance.getOverseasWarehouseCode());
|
||||
item.setSourceWarehouseName(balance.getOverseasWarehouseName());
|
||||
item.setCreatedAt(now); items.insert(item);
|
||||
}
|
||||
|
||||
private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) {
|
||||
LastlegOrder order = orders.selectById(value.getLastlegOrderId());
|
||||
String skuCodes = detailItems.stream().map(RelabelItem::getSkuCode).filter(Objects::nonNull).distinct().collect(Collectors.joining(","));
|
||||
String warehouses = detailItems.stream().map(RelabelItem::getDestinationWarehouseId)
|
||||
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(","));
|
||||
return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(),
|
||||
order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(),
|
||||
StringUtils.hasText(warehouses) ? warehouses : value.getOverseasWarehouseId(),
|
||||
value.getOverseasWarehouseId(),
|
||||
value.getLabelDeadline(), value.getContactName(), value.getContactPhone(), value.getStatus(),
|
||||
Objects.requireNonNullElse(value.getItemCount(), detailItems.size()),
|
||||
Objects.requireNonNullElse(value.getTotalPackageCount(), 0),
|
||||
@@ -267,6 +343,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
} else {
|
||||
value.setWalletStatus(result.operation() + "_FAILED"); value.setWalletErrorCode(result.errorCode());
|
||||
value.setWalletErrorMessage(result.errorMessage());
|
||||
if ("FREEZE".equals(result.operation())) {
|
||||
releaseInventory(value.getCustomerId(), "WALLET_CALLBACK", value);
|
||||
}
|
||||
}
|
||||
value.setUpdatedAt(LocalDateTime.now()); applications.updateById(value);
|
||||
}
|
||||
@@ -277,7 +356,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
value.getTotalPackageCount(), value.getNotifiedPackageCount(), value.getShipmentNo(),
|
||||
value.getTrackingCode(), value.getNewSkuCode(), value.getLabelRequirement(), value.getRemark(), value.getRelabelFileId(),
|
||||
value.getRelabelFileName(), value.getRelabelFileUrl(), value.getCartonMark(), value.getFbaCartonMark(),
|
||||
value.getDestinationWarehouseId());
|
||||
StringUtils.hasText(value.getDestinationFbaWarehouseCode()) ? value.getDestinationFbaWarehouseCode() : value.getDestinationWarehouseId(),
|
||||
operationType(value), readSkuItems(value.getSkuRelabelItemsJson()),
|
||||
value.getSourceInventoryBalanceId(), value.getSourceInventoryPositionId(), value.getSourceInboundOrderId(),
|
||||
value.getSourceInboundOrderNo(), value.getSourceInboundCode(), value.getSourceWarehouseId(),
|
||||
value.getSourceWarehouseCode(), value.getSourceWarehouseName());
|
||||
}
|
||||
|
||||
private LastlegOrder insertTailOrder(String customerId, RelabelRequests.Create request, String requestHash,
|
||||
@@ -310,18 +393,61 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return legacy == null ? List.of() : List.of(legacy);
|
||||
}
|
||||
|
||||
private static String destination(RelabelRequests.Item item, String fallback) {
|
||||
String value = StringUtils.hasText(item.destinationWarehouseId())
|
||||
? item.destinationWarehouseId().trim()
|
||||
: trim(fallback);
|
||||
if (!StringUtils.hasText(value)) throw badRequest("Destination warehouse is required for every carton");
|
||||
return value;
|
||||
private record SourceInventory(RelabelRequests.Item request, InventoryBalance balance) {}
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
|
||||
private void releaseInventory(String customerId, String operatorId, RelabelApplication application) {
|
||||
for (RelabelItem item : itemList(application.getId())) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getTotalPackageCount() == null) continue;
|
||||
inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
item.getSourceInventoryPositionId(), item.getTotalPackageCount(), application.getRelabelNo(), null,
|
||||
"RELABEL:" + application.getRelabelNo(), application.getIdempotencyKey() + ":RELEASE:"
|
||||
+ item.getSourceInventoryPositionId(), "贴标结束释放库存至原入仓单"));
|
||||
}
|
||||
}
|
||||
|
||||
private List<RelabelItem> itemList(Long applicationId) {
|
||||
return items.selectList(new LambdaQueryWrapper<RelabelItem>().eq(RelabelItem::getApplicationId, applicationId)
|
||||
.orderByAsc(RelabelItem::getId));
|
||||
}
|
||||
private void validateOperation(RelabelRequests.Item item) {
|
||||
String type = operationType(item);
|
||||
if (!Set.of("CARTON_LABEL", "SKU_LABEL", "CARTON_AND_SKU").contains(type)) {
|
||||
throw badRequest("Unsupported relabel operation type");
|
||||
}
|
||||
if (item.notifiedPackageCount() > item.totalPackageCount()) {
|
||||
throw badRequest("Notified relocation packages cannot exceed total packages for carton " + item.cartonMark());
|
||||
}
|
||||
if (("CARTON_LABEL".equals(type) || "CARTON_AND_SKU".equals(type))
|
||||
&& (!StringUtils.hasText(item.fbaCartonMark()) || !StringUtils.hasText(item.destinationFbaWarehouseCode()))) {
|
||||
throw badRequest("Carton label changes require both FBA carton mark and destination warehouse");
|
||||
}
|
||||
if (("SKU_LABEL".equals(type) || "CARTON_AND_SKU".equals(type))
|
||||
&& (item.skuItems() == null || item.skuItems().isEmpty())) {
|
||||
throw badRequest("SKU label changes require at least one SKU detail");
|
||||
}
|
||||
if ("CARTON_LABEL".equals(type) && !StringUtils.hasText(item.newSkuCode())) {
|
||||
throw badRequest("Carton label changes require the new SKU");
|
||||
}
|
||||
}
|
||||
private static String operationType(RelabelRequests.Item item) {
|
||||
return StringUtils.hasText(item.operationType()) ? item.operationType().trim().toUpperCase() : "CARTON_LABEL";
|
||||
}
|
||||
private static String operationType(RelabelItem item) {
|
||||
return StringUtils.hasText(item.getOperationType()) ? item.getOperationType() : "CARTON_LABEL";
|
||||
}
|
||||
private String writeSkuItems(List<RelabelRequests.SkuItem> values) {
|
||||
if (values == null || values.isEmpty()) return null;
|
||||
try { return objectMapper.writeValueAsString(values); }
|
||||
catch (Exception exception) { throw new IllegalStateException("Cannot save relabel SKU details", exception); }
|
||||
}
|
||||
private List<RelabelResponses.SkuItem> readSkuItems(String value) {
|
||||
if (!StringUtils.hasText(value)) return List.of();
|
||||
try {
|
||||
return objectMapper.readValue(value, objectMapper.getTypeFactory().constructCollectionType(
|
||||
List.class, RelabelResponses.SkuItem.class));
|
||||
} catch (Exception exception) { throw new IllegalStateException("Cannot read relabel SKU details", exception); }
|
||||
}
|
||||
private RelabelApplication require(String customerId, String relabelNo) {
|
||||
RelabelApplication value = StringUtils.hasText(customerId)
|
||||
? applications.findNo(customerId, relabelNo)
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/return-inventory")
|
||||
public class MerchantReturnInventoryController {
|
||||
public static final String LIST_PERMISSION = "merchant:overseas-warehouse:return-inventory:list";
|
||||
@Resource private ReturnInventoryService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')")
|
||||
public ApiResponse<PageResult<ReturnInventoryResponses.InventoryItem>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid ReturnInventoryRequests.Query request) {
|
||||
return ApiResponse.success(service.page(customer(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/summary")
|
||||
@PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')")
|
||||
public ApiResponse<PageResult<ReturnInventoryResponses.SummaryItem>> summary(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid ReturnInventoryRequests.SummaryQuery request) {
|
||||
return ApiResponse.success(service.summaryPage(customer(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}")
|
||||
@PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')")
|
||||
public ApiResponse<ReturnInventoryResponses.CartonDetail> carton(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.carton(customer(user), cartonMark));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}/label")
|
||||
@PreAuthorize("hasAuthority('" + LIST_PERMISSION + "')")
|
||||
public ApiResponse<ReturnInventoryResponses.Label> label(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.label(customer(user), cartonMark));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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.returnreceipt.dto.ReturnOrderRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnOrderResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnReceiptService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/return-orders")
|
||||
public class MerchantReturnOrderController {
|
||||
@Resource private ReturnReceiptService service;
|
||||
@PostMapping public ApiResponse<ReturnOrderResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReturnOrderRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||
@GetMapping public ApiResponse<PageResult<ReturnOrderResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@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)); }
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/return-inventory")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaReturnInventoryController {
|
||||
@Resource private ReturnInventoryService service;
|
||||
|
||||
@GetMapping("/packing/orders")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPreview> packingOrders(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid ReturnInventoryRequests.PackingQuery request) {
|
||||
return ApiResponse.success(service.packingPreview(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/cartons/merge")
|
||||
public ApiResponse<ReturnInventoryResponses.MergeResult> merge(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReturnInventoryRequests.Merge request) {
|
||||
return ApiResponse.success(service.merge(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}/label")
|
||||
public ApiResponse<ReturnInventoryResponses.Label> label(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.label(PdaWarehouseIdentity.require(user), cartonMark));
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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.returnreceipt.dto.PdaReturnReceiptRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.PdaReturnReceiptResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnReceiptService;
|
||||
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.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pda/v1/last-leg/return-receipts")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaReturnReceiptController {
|
||||
@Resource private ReturnReceiptService service;
|
||||
@PostMapping("/start") public ApiResponse<PdaReturnReceiptResponses.Start> start(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PdaReturnReceiptRequests.Start request) {
|
||||
return ApiResponse.success(service.start(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
@PostMapping("/sku-candidates") public ApiResponse<PdaReturnReceiptResponses.SkuCandidates> skuCandidates(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PdaReturnReceiptRequests.SkuLookup request) {
|
||||
return ApiResponse.success(service.skuCandidates(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
@PostMapping("/confirm") public ApiResponse<PdaReturnReceiptResponses.Confirmed> confirm(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PdaReturnReceiptRequests.Confirm request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.confirm(identity, request));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.lastleg.returnreceipt.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/return-inventory")
|
||||
public class PlatformReturnInventoryController {
|
||||
@Resource private ReturnInventoryService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')")
|
||||
public ApiResponse<PageResult<ReturnInventoryResponses.PlatformStock>> stock(@Valid ReturnInventoryRequests.PlatformQuery request) {
|
||||
return ApiResponse.success(service.platformStockPage(request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-cartons:list')")
|
||||
public ApiResponse<PageResult<ReturnInventoryResponses.PlatformCarton>> cartons(@Valid ReturnInventoryRequests.PlatformQuery request) {
|
||||
return ApiResponse.success(service.platformCartonPage(request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-cartons:list')")
|
||||
public ApiResponse<ReturnInventoryResponses.CartonDetail> carton(@PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.platformCarton(cartonMark));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}/label")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-cartons:list')")
|
||||
public ApiResponse<ReturnInventoryResponses.Label> label(@PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.platformLabel(cartonMark));
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class PdaReturnReceiptRequests {
|
||||
private PdaReturnReceiptRequests() { }
|
||||
public record Start(@NotBlank @Size(max = 128) String returnNo) { }
|
||||
public record SkuLookup(@NotBlank @Size(max = 128) String skuCode) { }
|
||||
public record Confirm(@NotBlank @Size(max = 128) String returnNo,
|
||||
@Size(max = 128) String trackingNo,
|
||||
@NotBlank @Size(max = 16) String startedBy,
|
||||
@Size(max = 128) String cartonMark,
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
public record Item(@NotBlank @Size(max = 128) String skuCode, @Positive long receivedQuantity) { }
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class PdaReturnReceiptResponses {
|
||||
private PdaReturnReceiptResponses() { }
|
||||
public record Start(String returnNo, String status, List<Item> expectedItems) { }
|
||||
public record SkuCandidates(String skuCode, List<Candidate> candidates) { }
|
||||
public record Candidate(String returnNo, String customerId, String customerReferenceNo, String status,
|
||||
long skuExpectedQuantity, long skuReceivedQuantity, long orderExpectedQuantity,
|
||||
long orderReceivedQuantity) { }
|
||||
public record Item(String skuCode, String productNameZh, long expectedQuantity, long receivedQuantity) { }
|
||||
public record Confirmed(String returnNo, String receiptNo, String receiptBatchNo, String cartonMark, String orderStatus,
|
||||
boolean fullyMatched, long expectedQuantity, long receivedQuantity,
|
||||
LocalDateTime signedAt) { }
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReturnInventoryRequests {
|
||||
private ReturnInventoryRequests() { }
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String overseasWarehouseId,
|
||||
String skuCode, String cartonMark, String status, String returnNo) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record SummaryQuery(@Min(1) long page, @Min(1) @Max(200) long size, String overseasWarehouseId,
|
||||
String skuCode) {
|
||||
public SummaryQuery { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record PlatformQuery(@Min(1) long page, @Min(1) @Max(200) long size, String customerId,
|
||||
String overseasWarehouseId, String skuCode, String cartonMark,
|
||||
String returnNo, String status) {
|
||||
public PlatformQuery { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record PackingQuery(@Size(max = 128) String returnNo) { }
|
||||
|
||||
public record Merge(@NotEmpty @Size(min = 1, max = 100) List<@NotBlank @Size(max = 128) String> returnNos,
|
||||
@NotEmpty @Size(max = 500) List<@Valid ValidItem> items,
|
||||
@NotNull @Positive BigDecimal length,
|
||||
@NotNull @Positive BigDecimal width,
|
||||
@NotNull @Positive BigDecimal height,
|
||||
@NotNull @Positive BigDecimal weight,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record ValidItem(@NotBlank @Size(max = 128) String skuCode, @Positive long quantity) { }
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReturnInventoryResponses {
|
||||
private ReturnInventoryResponses() { }
|
||||
|
||||
public record InventoryItem(String overseasWarehouseId, String cartonMark, String sourceBatchNo, String status, String skuCode,
|
||||
String productNameZh, long quantity, int cartonSkuTypes, long cartonTotalQuantity,
|
||||
LocalDateTime receivedAt) { }
|
||||
public record SummaryItem(String overseasWarehouseId, String skuCode, String productNameZh,
|
||||
long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { }
|
||||
public record CartonDetail(String cartonMark, String overseasWarehouseId, String sourceBatchNo, String sourceReturnNo, String status,
|
||||
int skuTypes, long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height,
|
||||
BigDecimal weight, LocalDateTime receivedAt, List<CartonItem> items) { }
|
||||
public record CartonItem(String skuCode, String productNameZh, long quantity) { }
|
||||
public record Label(String cartonMark, String overseasWarehouseId, String customerId, String sourceBatchNo, String sourceReturnNo,
|
||||
long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
List<CartonItem> items, LocalDateTime generatedAt, String printableHtml) { }
|
||||
public record MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label) { }
|
||||
public record PackingOrder(String returnNo, String customerId, String customerReferenceNo, String status) { }
|
||||
public record PackingSku(String skuCode, String productNameZh, long availableQuantity) { }
|
||||
public record PackingPreview(List<PackingOrder> orders, String customerId, List<PackingSku> skus) { }
|
||||
public record PlatformStock(String customerId, String overseasWarehouseId, String skuCode, String productNameZh,
|
||||
long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { }
|
||||
public record PlatformCarton(String customerId, String overseasWarehouseId, String cartonMark, String sourceReturnNo,
|
||||
String sourceBatchNo, String status, int skuTypes, long totalQuantity,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
LocalDateTime createdAt) { }
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class ReturnOrderRequests {
|
||||
private ReturnOrderRequests() { }
|
||||
public record Create(@NotBlank @Size(max = 128) String overseasWarehouseId,
|
||||
@Size(max = 128) String customerReferenceNo,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items) { }
|
||||
public record Item(@NotBlank @Size(max = 128) String skuCode,
|
||||
@Size(max = 255) String productNameZh,
|
||||
@Positive long expectedQuantity) { }
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String returnNo,
|
||||
String trackingNo, String skuCode, String status) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReturnOrderResponses {
|
||||
private ReturnOrderResponses() { }
|
||||
public record Summary(String returnNo, String overseasWarehouseId, String customerReferenceNo, String status,
|
||||
int skuTypes, long expectedQuantity, long receivedQuantity, LocalDateTime createdAt) { }
|
||||
public record Item(String skuCode, String productNameZh, long expectedQuantity,
|
||||
long receivedQuantity) { }
|
||||
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) { }
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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;
|
||||
|
||||
/** Customer-facing available return-inventory balance. */
|
||||
@Data
|
||||
@TableName("ll_return_inventory_balance")
|
||||
public class ReturnInventoryBalance {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String overseasWarehouseId;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long availableQuantity;
|
||||
private Long cartonQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** Physical carton that holds return inventory. */
|
||||
@Data
|
||||
@TableName("ll_return_inventory_carton")
|
||||
public class ReturnInventoryCarton {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String overseasWarehouseId;
|
||||
private Long sourceReceiptId;
|
||||
private String sourceBatchNo;
|
||||
private String sourceReturnNo;
|
||||
private String cartonMark;
|
||||
private String status;
|
||||
private Integer skuTypes;
|
||||
private Long totalQuantity;
|
||||
private BigDecimal length;
|
||||
private BigDecimal width;
|
||||
private BigDecimal height;
|
||||
private BigDecimal weight;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime mergedAt;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.returnreceipt.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ll_return_inventory_carton_item")
|
||||
public class ReturnInventoryCartonItem {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long cartonId;
|
||||
private String customerId;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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_return_inventory_operation")
|
||||
public class ReturnInventoryOperation {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String operatorId;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String operationType;
|
||||
private Long resultCartonId;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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_return_order")
|
||||
public class ReturnOrder {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String returnNo;
|
||||
private String overseasWarehouseId;
|
||||
private String customerReferenceNo;
|
||||
private String status;
|
||||
private Integer expectedSkuTypes;
|
||||
private Long expectedQuantity;
|
||||
private Long receivedQuantity;
|
||||
private String remark;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String createdBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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_return_order_item")
|
||||
public class ReturnOrderItem {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long returnOrderId;
|
||||
private String customerId;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long expectedQuantity;
|
||||
private Long receivedQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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_return_receipt")
|
||||
public class ReturnReceipt {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long returnOrderId;
|
||||
private String customerId;
|
||||
private String receiptNo;
|
||||
private String receiptBatchNo;
|
||||
private String cartonMark;
|
||||
private String trackingNo;
|
||||
private String boundBy;
|
||||
private String status;
|
||||
private Long receivedQuantity;
|
||||
private String operatorId;
|
||||
private String operatorName;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private LocalDateTime signedAt;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.returnreceipt.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;
|
||||
|
||||
/** A completed receipt starts the storage-billing clock, even when the whole return order remains partial. */
|
||||
@Data
|
||||
@TableName("ll_return_receipt_batch")
|
||||
public class ReturnReceiptBatch {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long returnOrderId;
|
||||
private Long receiptId;
|
||||
private String customerId;
|
||||
private String batchNo;
|
||||
private String overseasWarehouseId;
|
||||
private String storageBillingStatus;
|
||||
private LocalDateTime storageStartedAt;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.returnreceipt.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ll_return_receipt_item")
|
||||
public class ReturnReceiptItem {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long receiptId;
|
||||
private Long returnOrderItemId;
|
||||
private String customerId;
|
||||
private String skuCode;
|
||||
private Long receivedQuantity;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
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 ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
|
||||
@Insert("INSERT INTO ll_return_inventory_balance (id,customer_id,overseas_warehouse_id,sku_code,product_name_zh,available_quantity,carton_quantity,created_at,updated_at) " +
|
||||
"VALUES (#{id},#{customerId},#{warehouseId},#{skuCode},#{productNameZh},#{quantity},#{cartons},NOW(3),NOW(3)) " +
|
||||
"ON DUPLICATE KEY UPDATE available_quantity=available_quantity+VALUES(available_quantity),carton_quantity=carton_quantity+VALUES(carton_quantity), " +
|
||||
"product_name_zh=COALESCE(NULLIF(VALUES(product_name_zh),''),product_name_zh),updated_at=NOW(3)")
|
||||
int adjust(@Param("id") long id, @Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("productNameZh") String productNameZh,
|
||||
@Param("quantity") long quantity, @Param("cartons") long cartons);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if></script>")
|
||||
long countPage(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if>" +
|
||||
"ORDER BY updated_at DESC,id DESC LIMIT #{offset},#{size}</script>")
|
||||
List<ReturnInventoryBalance> page(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("offset") long offset, @Param("size") long size);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_return_inventory_balance WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND customer_id LIKE CONCAT('%',#{customerId},'%') </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if></script>")
|
||||
long platformCount(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_return_inventory_balance WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND customer_id LIKE CONCAT('%',#{customerId},'%') </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if>" +
|
||||
"ORDER BY updated_at DESC,id DESC LIMIT #{offset},#{size}</script>")
|
||||
List<ReturnInventoryBalance> platformPage(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("offset") long offset, @Param("size") long size);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
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 ReturnInventoryCartonItemMapper extends BaseMapper<ReturnInventoryCartonItem> {
|
||||
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} ORDER BY sku_code")
|
||||
List<ReturnInventoryCartonItem> listCarton(@Param("cartonId") long cartonId);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
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 ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1")
|
||||
ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1 FOR UPDATE")
|
||||
ReturnInventoryCarton lockMark(@Param("cartonMark") String cartonMark);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} LIMIT 1")
|
||||
ReturnInventoryCarton findReceipt(@Param("receiptId") long receiptId);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND carton_mark=#{cartonMark} LIMIT 1")
|
||||
ReturnInventoryCarton findCustomerMark(@Param("customerId") String customerId, @Param("cartonMark") String cartonMark);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_return_inventory_carton c WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND c.customer_id LIKE CONCAT('%',#{customerId},'%') </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND c.overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='cartonMark != null and cartonMark != \"\"'>AND c.carton_mark LIKE CONCAT('%',#{cartonMark},'%') </if>" +
|
||||
"<if test='returnNo != null and returnNo != \"\"'>AND c.source_return_no LIKE CONCAT('%',#{returnNo},'%') </if>" +
|
||||
"<if test='status != null and status != \"\"'>AND c.status=#{status} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND EXISTS(SELECT 1 FROM ll_return_inventory_carton_item i WHERE i.carton_id=c.id AND i.sku_code LIKE CONCAT('%',#{skuCode},'%')) </if></script>")
|
||||
long platformCount(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("cartonMark") String cartonMark, @Param("returnNo") String returnNo, @Param("status") String status, @Param("skuCode") String skuCode);
|
||||
|
||||
@Select("<script>SELECT c.* FROM ll_return_inventory_carton c WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND c.customer_id LIKE CONCAT('%',#{customerId},'%') </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND c.overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='cartonMark != null and cartonMark != \"\"'>AND c.carton_mark LIKE CONCAT('%',#{cartonMark},'%') </if>" +
|
||||
"<if test='returnNo != null and returnNo != \"\"'>AND c.source_return_no LIKE CONCAT('%',#{returnNo},'%') </if>" +
|
||||
"<if test='status != null and status != \"\"'>AND c.status=#{status} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND EXISTS(SELECT 1 FROM ll_return_inventory_carton_item i WHERE i.carton_id=c.id AND i.sku_code LIKE CONCAT('%',#{skuCode},'%')) </if>" +
|
||||
"ORDER BY c.created_at DESC,c.id DESC LIMIT #{offset},#{size}</script>")
|
||||
List<ReturnInventoryCarton> platformPage(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("cartonMark") String cartonMark, @Param("returnNo") String returnNo, @Param("status") String status,
|
||||
@Param("skuCode") String skuCode, @Param("offset") long offset, @Param("size") long size);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOperation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryOperationMapper extends BaseMapper<ReturnInventoryOperation> {
|
||||
@Select("SELECT * FROM ll_return_inventory_operation WHERE operator_id=#{operatorId} AND idempotency_key=#{key} LIMIT 1")
|
||||
ReturnInventoryOperation findKey(@Param("operatorId") String operatorId, @Param("key") String key);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.model.ReturnInventoryRow;
|
||||
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 ReturnInventoryQueryMapper {
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id " +
|
||||
"WHERE c.customer_id=#{customerId} " +
|
||||
"<if test='status != null and status != \"\"'>AND c.status=#{status} </if>" +
|
||||
"<if test='returnNo != null and returnNo != \"\"'>AND (c.source_return_no LIKE CONCAT('%',#{returnNo},'%') OR EXISTS (SELECT 1 FROM ll_return_receipt r JOIN ll_return_order o ON o.id=r.return_order_id WHERE r.id=c.source_receipt_id AND o.return_no LIKE CONCAT('%',#{returnNo},'%'))) </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND c.overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND i.sku_code LIKE CONCAT('%',#{skuCode},'%') </if>" +
|
||||
"<if test='cartonMark != null and cartonMark != \"\"'>AND c.carton_mark LIKE CONCAT('%',#{cartonMark},'%') </if></script>")
|
||||
long count(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("cartonMark") String cartonMark,
|
||||
@Param("status") String status, @Param("returnNo") String returnNo);
|
||||
|
||||
@Select("<script>SELECT c.id carton_id,c.overseas_warehouse_id,c.carton_mark,c.source_batch_no,c.status,c.sku_types,c.total_quantity,c.created_at, " +
|
||||
"i.sku_code,i.product_name_zh,i.quantity FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id " +
|
||||
"WHERE c.customer_id=#{customerId} " +
|
||||
"<if test='status != null and status != \"\"'>AND c.status=#{status} </if>" +
|
||||
"<if test='returnNo != null and returnNo != \"\"'>AND (c.source_return_no LIKE CONCAT('%',#{returnNo},'%') OR EXISTS (SELECT 1 FROM ll_return_receipt r JOIN ll_return_order o ON o.id=r.return_order_id WHERE r.id=c.source_receipt_id AND o.return_no LIKE CONCAT('%',#{returnNo},'%'))) </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND c.overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND i.sku_code LIKE CONCAT('%',#{skuCode},'%') </if>" +
|
||||
"<if test='cartonMark != null and cartonMark != \"\"'>AND c.carton_mark LIKE CONCAT('%',#{cartonMark},'%') </if>" +
|
||||
"ORDER BY c.created_at DESC,c.id DESC,i.sku_code LIMIT #{offset},#{size}</script>")
|
||||
List<ReturnInventoryRow> page(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("cartonMark") String cartonMark,
|
||||
@Param("status") String status, @Param("returnNo") String returnNo,
|
||||
@Param("offset") long offset, @Param("size") long size);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrderItem;
|
||||
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 ReturnOrderItemMapper extends BaseMapper<ReturnOrderItem> {
|
||||
@Select("SELECT * FROM ll_return_order_item WHERE return_order_id=#{orderId} ORDER BY sku_code,id")
|
||||
List<ReturnOrderItem> listOrder(@Param("orderId") Long orderId);
|
||||
@Select("SELECT * FROM ll_return_order_item WHERE return_order_id=#{orderId} ORDER BY sku_code,id FOR UPDATE")
|
||||
List<ReturnOrderItem> lockOrder(@Param("orderId") Long orderId);
|
||||
|
||||
@Select("SELECT i.* FROM ll_return_order_item i JOIN ll_return_order o ON o.id=i.return_order_id " +
|
||||
"WHERE o.overseas_warehouse_id=#{warehouseId} AND o.status IN ('PENDING_RECEIPT','PARTIALLY_RECEIVED') " +
|
||||
"AND i.sku_code=#{skuCode} AND i.received_quantity<i.expected_quantity ORDER BY o.created_at,o.id LIMIT 50")
|
||||
List<ReturnOrderItem> activeSkuCandidates(@Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
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 ReturnOrderMapper extends BaseMapper<ReturnOrder> {
|
||||
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
ReturnOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND return_no=#{returnNo} LIMIT 1")
|
||||
ReturnOrder findNo(@Param("customerId") String customerId, @Param("returnNo") String returnNo);
|
||||
@Select("SELECT * FROM ll_return_order WHERE return_no=#{returnNo} LIMIT 1 FOR UPDATE")
|
||||
ReturnOrder lockNo(@Param("returnNo") String returnNo);
|
||||
@Select("SELECT * FROM ll_return_order WHERE return_no=#{returnNo} AND overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND status IN ('PENDING_RECEIPT','PARTIALLY_RECEIVED') LIMIT 1")
|
||||
ReturnOrder findActiveNo(@Param("warehouseId") String warehouseId, @Param("returnNo") String returnNo);
|
||||
@Select("SELECT * FROM ll_return_order WHERE overseas_warehouse_id=#{warehouseId} AND status='COMPLETED' " +
|
||||
"AND return_no LIKE CONCAT('%',#{returnNo},'%') ORDER BY created_at DESC,id DESC LIMIT 100")
|
||||
List<ReturnOrder> findCompletedLike(@Param("warehouseId") String warehouseId, @Param("returnNo") String returnNo);
|
||||
@Select("<script>SELECT * FROM ll_return_order WHERE overseas_warehouse_id=#{warehouseId} AND return_no IN " +
|
||||
"<foreach collection='returnNos' item='returnNo' open='(' separator=',' close=')'>#{returnNo}</foreach></script>")
|
||||
List<ReturnOrder> findByNos(@Param("warehouseId") String warehouseId, @Param("returnNos") List<String> returnNos);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceiptBatch;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnReceiptBatchMapper extends BaseMapper<ReturnReceiptBatch> { }
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceiptItem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnReceiptItemMapper extends BaseMapper<ReturnReceiptItem> { }
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceipt;
|
||||
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 ReturnReceiptMapper extends BaseMapper<ReturnReceipt> {
|
||||
@Select("SELECT * FROM ll_return_receipt WHERE operator_id=#{operatorId} AND idempotency_key=#{key} LIMIT 1")
|
||||
ReturnReceipt findKey(@Param("operatorId") String operatorId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_return_receipt WHERE return_order_id=#{orderId} ORDER BY signed_at DESC,id DESC")
|
||||
List<ReturnReceipt> listOrder(@Param("orderId") Long orderId);
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.lastleg.returnreceipt.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class ReturnInventoryRow {
|
||||
private Long cartonId;
|
||||
private String overseasWarehouseId;
|
||||
private String cartonMark;
|
||||
private String sourceBatchNo;
|
||||
private String status;
|
||||
private Integer skuTypes;
|
||||
private Long totalQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
package com.baiyue.lastleg.returnreceipt.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOperation;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceipt;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryOperationMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryQueryMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.model.ReturnInventoryRow;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Return stock is deliberately separate from normal inbound stock because it has no putaway location yet. */
|
||||
@Service
|
||||
public class ReturnInventoryService {
|
||||
public static final String AVAILABLE = "AVAILABLE";
|
||||
public static final String MERGED = "MERGED";
|
||||
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper cartonItems;
|
||||
@Resource private ReturnInventoryBalanceMapper balances;
|
||||
@Resource private ReturnInventoryOperationMapper operations;
|
||||
@Resource private ReturnInventoryQueryMapper queries;
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper orders;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper receiptMapper;
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryCarton createReceiptCarton(ReturnOrder order, ReturnReceipt receipt,
|
||||
String requestedMark, List<ReceiptLine> lines) {
|
||||
if (lines.isEmpty()) throw new IllegalArgumentException("退货签收库存明细不能为空");
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
if (cartons.findMark(cartonMark) != null) throw conflict("箱号已存在: " + cartonMark);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long total = lines.stream().mapToLong(ReceiptLine::quantity).sum();
|
||||
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.setStatus(AVAILABLE); carton.setSkuTypes(lines.size()); carton.setTotalQuantity(total); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
for (ReceiptLine line : lines) {
|
||||
ReturnInventoryCartonItem item = new ReturnInventoryCartonItem();
|
||||
item.setId(IdWorker.getId()); item.setCartonId(cartonId); item.setCustomerId(order.getCustomerId());
|
||||
item.setSkuCode(line.skuCode()); item.setProductNameZh(text(line.productNameZh())); item.setQuantity(line.quantity());
|
||||
cartonItems.insert(item);
|
||||
balances.adjust(IdWorker.getId(), order.getCustomerId(), order.getOverseasWarehouseId(), line.skuCode(),
|
||||
text(line.productNameZh()), line.quantity(), 1);
|
||||
}
|
||||
return carton;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.InventoryItem> page(String customerId, ReturnInventoryRequests.Query request) {
|
||||
String warehouseId = text(request.overseasWarehouseId());
|
||||
String sku = text(request.skuCode());
|
||||
String carton = text(request.cartonMark());
|
||||
long total = queries.count(customerId, warehouseId, sku, carton, text(request.status()), text(request.returnNo()));
|
||||
List<ReturnInventoryResponses.InventoryItem> records = total == 0 ? List.of() : queries.page(customerId, warehouseId, sku,
|
||||
carton, text(request.status()), text(request.returnNo()), (request.page() - 1) * request.size(), request.size()).stream().map(this::item).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.SummaryItem> summaryPage(String customerId, ReturnInventoryRequests.SummaryQuery request) {
|
||||
String warehouseId = text(request.overseasWarehouseId());
|
||||
String sku = text(request.skuCode());
|
||||
long total = balances.countPage(customerId, warehouseId, sku);
|
||||
List<ReturnInventoryResponses.SummaryItem> records = total == 0 ? List.of() : balances.page(customerId, warehouseId, sku,
|
||||
(request.page() - 1) * request.size(), request.size()).stream().map(this::summary).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.PlatformStock> platformStockPage(ReturnInventoryRequests.PlatformQuery request) {
|
||||
String customerId = text(request.customerId()); String warehouseId = text(request.overseasWarehouseId()); String sku = text(request.skuCode());
|
||||
long total = balances.platformCount(customerId, warehouseId, sku);
|
||||
List<ReturnInventoryResponses.PlatformStock> records = total == 0 ? List.of() : balances.platformPage(customerId, warehouseId, sku,
|
||||
(request.page() - 1) * request.size(), request.size()).stream().map(balance -> new ReturnInventoryResponses.PlatformStock(
|
||||
balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(),
|
||||
value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt())).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.PlatformCarton> platformCartonPage(ReturnInventoryRequests.PlatformQuery request) {
|
||||
String customerId = text(request.customerId()); String warehouseId = text(request.overseasWarehouseId());
|
||||
String cartonMark = text(request.cartonMark()); String returnNo = text(request.returnNo()); String status = normStatus(request.status()); String sku = text(request.skuCode());
|
||||
long total = cartons.platformCount(customerId, warehouseId, cartonMark, returnNo, status, sku);
|
||||
List<ReturnInventoryResponses.PlatformCarton> records = total == 0 ? List.of() : cartons.platformPage(customerId, warehouseId, cartonMark,
|
||||
returnNo, status, sku, (request.page() - 1) * request.size(), request.size()).stream().map(carton -> new ReturnInventoryResponses.PlatformCarton(
|
||||
carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getSourceBatchNo(),
|
||||
carton.getStatus(), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(),
|
||||
carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt())).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.CartonDetail platformCarton(String cartonMark) {
|
||||
ReturnInventoryCarton carton = cartons.findMark(required(cartonMark, "箱号不能为空"));
|
||||
if (carton == null) throw notFound("退货箱不存在");
|
||||
return cartonDetail(carton);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.Label platformLabel(String cartonMark) {
|
||||
ReturnInventoryCarton carton = cartons.findMark(required(cartonMark, "箱号不能为空"));
|
||||
if (carton == null) throw notFound("退货箱不存在");
|
||||
return label(carton);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.CartonDetail carton(String customerId, String cartonMark) {
|
||||
ReturnInventoryCarton carton = cartons.findCustomerMark(customerId, required(cartonMark, "箱号不能为空"));
|
||||
if (carton == null) throw notFound("退货箱不存在");
|
||||
return cartonDetail(carton);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.Label label(String customerId, String cartonMark) {
|
||||
ReturnInventoryCarton carton = cartons.findCustomerMark(customerId, required(cartonMark, "箱号不能为空"));
|
||||
if (carton == null) throw notFound("退货箱不存在");
|
||||
return label(carton);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.Label label(PdaWarehouseIdentity identity, String cartonMark) {
|
||||
ReturnInventoryCarton carton = cartons.findMark(required(cartonMark, "箱号不能为空"));
|
||||
if (carton == null || !String.valueOf(identity.overseasWarehouseId()).equals(carton.getOverseasWarehouseId())) throw notFound("当前仓没有该退货箱");
|
||||
return label(carton);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.PackingPreview packingPreview(PdaWarehouseIdentity identity, ReturnInventoryRequests.PackingQuery request) {
|
||||
List<ReturnOrder> candidates = orders.findCompletedLike(String.valueOf(identity.overseasWarehouseId()), text(request.returnNo()) == null ? "" : text(request.returnNo()));
|
||||
Map<String, ReturnOrder> unique = new LinkedHashMap<>();
|
||||
candidates.forEach(order -> unique.put(order.getReturnNo(), order));
|
||||
List<ReturnOrder> selected = unique.values().stream().limit(100).toList();
|
||||
String customerId = selected.size() == 1 ? selected.get(0).getCustomerId() : null;
|
||||
Map<String, Long> quantities = new LinkedHashMap<>();
|
||||
Map<String, String> names = new LinkedHashMap<>();
|
||||
for (ReturnOrder order : selected) {
|
||||
for (ReturnReceipt receipt : receiptMapper.listOrder(order.getId())) {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
if (carton == null || !AVAILABLE.equals(carton.getStatus())) continue;
|
||||
for (ReturnInventoryCartonItem item : cartonItems.listCarton(carton.getId())) {
|
||||
quantities.merge(item.getSkuCode(), value(item.getQuantity()), Math::addExact);
|
||||
names.putIfAbsent(item.getSkuCode(), item.getProductNameZh());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ReturnInventoryResponses.PackingPreview(selected.stream().map(order ->
|
||||
new ReturnInventoryResponses.PackingOrder(order.getReturnNo(), order.getCustomerId(), order.getCustomerReferenceNo(), order.getStatus())).toList(),
|
||||
customerId, quantities.entrySet().stream().map(entry -> new ReturnInventoryResponses.PackingSku(entry.getKey(), names.get(entry.getKey()), entry.getValue())).toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.MergeResult merge(PdaWarehouseIdentity identity, ReturnInventoryRequests.Merge request) {
|
||||
String requestHash = hash(request);
|
||||
String key = text(request.idempotencyKey());
|
||||
ReturnInventoryOperation replay = operations.findKey(identity.operatorKey(), key);
|
||||
if (replay != null) return replayMerge(replay, requestHash);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ReturnInventoryOperation operation = new ReturnInventoryOperation();
|
||||
operation.setId(IdWorker.getId()); operation.setOperatorId(identity.operatorKey()); operation.setIdempotencyKey(key);
|
||||
operation.setRequestHash(requestHash); operation.setOperationType("MERGE"); operation.setCreatedAt(now); operation.setUpdatedAt(now);
|
||||
try { operations.insert(operation); }
|
||||
catch (DuplicateKeyException concurrent) {
|
||||
ReturnInventoryOperation existing = operations.findKey(identity.operatorKey(), key);
|
||||
if (existing != null) return replayMerge(existing, requestHash);
|
||||
throw concurrent;
|
||||
}
|
||||
|
||||
List<String> returnNos = request.returnNos().stream().map(this::requiredReturnNo).distinct().toList();
|
||||
List<ReturnOrder> selectedOrders = orders.findByNos(String.valueOf(identity.overseasWarehouseId()), returnNos);
|
||||
if (selectedOrders.size() != returnNos.size()) throw notFound("存在不属于当前仓或不存在的退货单");
|
||||
String customerId = selectedOrders.get(0).getCustomerId();
|
||||
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<>();
|
||||
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()));
|
||||
}
|
||||
Map<String, Long> requested = new LinkedHashMap<>();
|
||||
Map<String, String> productNames = new LinkedHashMap<>();
|
||||
for (ReturnInventoryRequests.ValidItem item : request.items()) {
|
||||
String sku = required(item.skuCode(), "SKU 不能为空").toUpperCase(java.util.Locale.ROOT);
|
||||
if (requested.put(sku, item.quantity()) != null) throw badRequest("合箱 SKU 不能重复");
|
||||
}
|
||||
Map<String, Long> remaining = new LinkedHashMap<>(requested);
|
||||
Map<String, Integer> emptiedBySku = new LinkedHashMap<>();
|
||||
long total = 0;
|
||||
for (ReturnInventoryCarton source : sources) {
|
||||
List<ReturnInventoryCartonItem> sourceItems = cartonItems.listCarton(source.getId());
|
||||
List<String> originalSkus = sourceItems.stream().map(item -> item.getSkuCode().toUpperCase(java.util.Locale.ROOT)).toList();
|
||||
long sourceTotal = 0; int sourceTypes = 0;
|
||||
for (ReturnInventoryCartonItem item : sourceItems) {
|
||||
String sku = item.getSkuCode().toUpperCase(java.util.Locale.ROOT);
|
||||
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);
|
||||
productNames.putIfAbsent(sku, item.getProductNameZh());
|
||||
item.setQuantity(item.getQuantity() - take);
|
||||
if (item.getQuantity() == 0) cartonItems.deleteById(item.getId()); else cartonItems.updateById(item);
|
||||
}
|
||||
if (item.getQuantity() > 0) { sourceTotal = Math.addExact(sourceTotal, item.getQuantity()); sourceTypes++; }
|
||||
}
|
||||
if (sourceTotal == 0) {
|
||||
source.setStatus(MERGED); source.setMergedAt(now);
|
||||
originalSkus.forEach(sku -> emptiedBySku.merge(sku, 1, Integer::sum));
|
||||
}
|
||||
source.setTotalQuantity(sourceTotal); source.setSkuTypes(sourceTypes); source.setUpdatedAt(now); cartons.updateById(source);
|
||||
}
|
||||
if (remaining.values().stream().anyMatch(value -> value > 0)) throw conflict("合箱数量超过对应退货单的可用库存");
|
||||
if (total <= 0) throw badRequest("至少填写一个大于 0 的 SKU 合箱数量");
|
||||
long targetId = IdWorker.getId();
|
||||
ReturnInventoryCarton target = new ReturnInventoryCarton();
|
||||
target.setId(targetId); target.setCustomerId(customerId); target.setOverseasWarehouseId(sources.get(0).getOverseasWarehouseId());
|
||||
target.setCartonMark(numbers.next("RETURN_CARTON", target.getOverseasWarehouseId(), targetId)); target.setSourceBatchNo("MERGED"); target.setSourceReturnNo(String.join(",", returnNos)); target.setStatus(AVAILABLE);
|
||||
target.setSkuTypes(requested.size()); target.setTotalQuantity(total); target.setLength(request.length()); target.setWidth(request.width()); target.setHeight(request.height()); target.setWeight(request.weight()); target.setCreatedAt(now); target.setUpdatedAt(now); cartons.insert(target);
|
||||
for (Map.Entry<String, Long> entry : requested.entrySet()) {
|
||||
ReturnInventoryCartonItem targetItem = new ReturnInventoryCartonItem();
|
||||
targetItem.setId(IdWorker.getId()); targetItem.setCartonId(targetId); targetItem.setCustomerId(customerId); targetItem.setSkuCode(entry.getKey());
|
||||
targetItem.setProductNameZh(productNames.get(entry.getKey())); targetItem.setQuantity(entry.getValue()); cartonItems.insert(targetItem);
|
||||
balances.adjust(IdWorker.getId(), customerId, target.getOverseasWarehouseId(), entry.getKey(), productNames.get(entry.getKey()), 0,
|
||||
1L - emptiedBySku.getOrDefault(entry.getKey(), 0));
|
||||
}
|
||||
operation.setResultCartonId(targetId); operation.setUpdatedAt(now); operations.updateById(operation);
|
||||
return new ReturnInventoryResponses.MergeResult(target.getCartonMark(), requested.keySet().iterator().next(), total, label(target));
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.MergeResult replayMerge(ReturnInventoryOperation operation, String requestHash) {
|
||||
if (!requestHash.equals(operation.getRequestHash())) throw conflict("幂等键已用于不同的合箱请求");
|
||||
if (operation.getResultCartonId() == null) throw conflict("合箱请求正在处理");
|
||||
ReturnInventoryCarton carton = cartons.selectById(operation.getResultCartonId());
|
||||
if (carton == null) throw new IllegalStateException("合箱结果箱不存在");
|
||||
List<ReturnInventoryCartonItem> items = cartonItems.listCarton(carton.getId());
|
||||
return new ReturnInventoryResponses.MergeResult(carton.getCartonMark(), items.get(0).getSkuCode(), carton.getTotalQuantity(), label(carton));
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.Label label(ReturnInventoryCarton carton) {
|
||||
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList();
|
||||
return new ReturnInventoryResponses.Label(carton.getCartonMark(), carton.getOverseasWarehouseId(), carton.getCustomerId(),
|
||||
carton.getSourceBatchNo(), carton.getSourceReturnNo(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), items, LocalDateTime.now(), printableHtml(carton, items));
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.InventoryItem item(ReturnInventoryRow row) {
|
||||
return new ReturnInventoryResponses.InventoryItem(row.getOverseasWarehouseId(), row.getCartonMark(), row.getSourceBatchNo(), row.getStatus(),
|
||||
row.getSkuCode(), row.getProductNameZh(), value(row.getQuantity()), row.getSkuTypes() == null ? 0 : row.getSkuTypes(),
|
||||
value(row.getTotalQuantity()), row.getCreatedAt());
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.SummaryItem summary(com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance balance) {
|
||||
return new ReturnInventoryResponses.SummaryItem(balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(),
|
||||
value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt());
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.CartonDetail cartonDetail(ReturnInventoryCarton carton) {
|
||||
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList();
|
||||
return new ReturnInventoryResponses.CartonDetail(carton.getCartonMark(), carton.getOverseasWarehouseId(), carton.getSourceBatchNo(), carton.getSourceReturnNo(),
|
||||
carton.getStatus(), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), items);
|
||||
}
|
||||
|
||||
private static String printableHtml(ReturnInventoryCarton carton, List<ReturnInventoryResponses.CartonItem> items) {
|
||||
StringBuilder rows = new StringBuilder();
|
||||
for (ReturnInventoryResponses.CartonItem item : items) rows.append("<tr><td>").append(escape(item.skuCode())).append("</td><td>")
|
||||
.append(escape(item.productNameZh())).append("</td><td class=\"qty\">").append(item.quantity()).append("</td></tr>");
|
||||
return "<!doctype html><html><head><meta charset=\"utf-8\"><title>退货箱唛</title><style>"
|
||||
+ "@page{size:100mm 150mm;margin:5mm}body{font-family:Arial,'Microsoft YaHei',sans-serif;color:#111;margin:0}.label{border:2px solid #111;padding:5mm;min-height:136mm}.brand{font-size:16px;font-weight:700}.title{font-size:22px;font-weight:700;margin:3mm 0 5mm}.mark{font:700 18px monospace;letter-spacing:1px;word-break:break-all}.meta{font-size:11px;margin:4mm 0;border-top:1px solid #111;border-bottom:1px solid #111;padding:3mm 0}.barcode{height:11mm;margin:3mm 0;background:repeating-linear-gradient(90deg,#111 0 2px,#fff 2px 4px,#111 4px 5px,#fff 5px 8px)}table{width:100%;border-collapse:collapse;font-size:11px}th,td{border:1px solid #111;padding:2mm;text-align:left}.qty{text-align:right;font-weight:700}.total{font-size:14px;font-weight:700;text-align:right;margin-top:4mm}</style></head><body><div class=\"label\"><div class=\"brand\">BAIYUE OVERSEAS WAREHOUSE</div><div class=\"title\">RETURN CARTON LABEL / 退货箱唛</div><div class=\"mark\">"
|
||||
+ escape(carton.getCartonMark()) + "</div><div class=\"barcode\" aria-label=\"" + escape(carton.getCartonMark()) + "\"></div>"
|
||||
+ "<div class=\"meta\">仓库:" + escape(carton.getOverseasWarehouseId()) + "<br>客户:" + escape(carton.getCustomerId())
|
||||
+ "<br>箱规:" + escape(String.valueOf(carton.getLength())) + " × " + escape(String.valueOf(carton.getWidth())) + " × " + escape(String.valueOf(carton.getHeight())) + " cm / " + escape(String.valueOf(carton.getWeight())) + " kg"
|
||||
+ "<br>签收批次:" + escape(carton.getSourceBatchNo()) + "</div><table><thead><tr><th>SKU</th><th>品名</th><th>数量</th></tr></thead><tbody>"
|
||||
+ rows + "</tbody></table><div class=\"total\">合计:" + carton.getTotalQuantity() + " 件 / " + carton.getSkuTypes() + " 种 SKU</div></div></body></html>";
|
||||
}
|
||||
private String hash(Object request) { try { return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(String.valueOf(request).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (Exception exception) { throw new IllegalStateException("无法生成请求哈希", exception); } }
|
||||
private static String escape(String value) { return text(value) == null ? "" : text(value).replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); }
|
||||
private static long value(Long number) { return number == null ? 0 : number; }
|
||||
private static String required(String value, String message) { String result = text(value); if (result == null) throw badRequest(message); return result; }
|
||||
private String requiredReturnNo(String value) { return required(value, "退货单号不能为空"); }
|
||||
private static String text(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String normStatus(String value) { String result = text(value); return result == null ? null : result.toUpperCase(java.util.Locale.ROOT); }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
|
||||
public record ReceiptLine(String skuCode, String productNameZh, long quantity) { }
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package com.baiyue.lastleg.returnreceipt.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.PdaReturnReceiptRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.PdaReturnReceiptResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnOrderRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnOrderResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrderItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceipt;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceiptBatch;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnReceiptItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptBatchMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** Customer return forecasts and PDA receipt facts. A receipt batch is the storage-fee start fact. */
|
||||
@Service
|
||||
public class ReturnReceiptService {
|
||||
public static final String PENDING_RECEIPT = "PENDING_RECEIPT";
|
||||
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
|
||||
@Resource private ReturnOrderMapper orders;
|
||||
@Resource private ReturnOrderItemMapper orderItems;
|
||||
@Resource private ReturnReceiptMapper receipts;
|
||||
@Resource private ReturnReceiptItemMapper receiptItems;
|
||||
@Resource private ReturnReceiptBatchMapper batches;
|
||||
@Resource private ReturnInventoryService inventory;
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail create(String customerId, String operatorId, ReturnOrderRequests.Create request) {
|
||||
String key = text(request.idempotencyKey());
|
||||
String requestHash = hash(request);
|
||||
ReturnOrder replay = orders.findKey(customerId, key);
|
||||
if (replay != null) return replayOrder(customerId, replay, requestHash);
|
||||
Set<String> skuCodes = new LinkedHashSet<>();
|
||||
long expected = 0;
|
||||
for (ReturnOrderRequests.Item item : request.items()) {
|
||||
String skuCode = text(item.skuCode());
|
||||
if (!skuCodes.add(norm(skuCode))) throw badRequest("同一退货单不能重复预报相同 SKU: " + skuCode);
|
||||
expected = Math.addExact(expected, item.expectedQuantity());
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ReturnOrder order = new ReturnOrder();
|
||||
order.setId(IdWorker.getId()); order.setCustomerId(customerId);
|
||||
order.setOverseasWarehouseId(text(request.overseasWarehouseId()));
|
||||
order.setReturnNo(numbers.next("RETURN", order.getOverseasWarehouseId(), order.getId()));
|
||||
order.setCustomerReferenceNo(text(request.customerReferenceNo())); order.setStatus(PENDING_RECEIPT);
|
||||
order.setExpectedSkuTypes(skuCodes.size()); order.setExpectedQuantity(expected); order.setReceivedQuantity(0L);
|
||||
order.setRemark(text(request.remark())); order.setIdempotencyKey(key); order.setRequestHash(requestHash);
|
||||
order.setCreatedBy(text(operatorId)); order.setCreatedAt(now); order.setUpdatedAt(now);
|
||||
try { orders.insert(order); }
|
||||
catch (DuplicateKeyException concurrent) {
|
||||
ReturnOrder existing = orders.findKey(customerId, key);
|
||||
if (existing != null) return replayOrder(customerId, existing, requestHash);
|
||||
throw concurrent;
|
||||
}
|
||||
for (ReturnOrderRequests.Item input : request.items()) {
|
||||
ReturnOrderItem item = new ReturnOrderItem();
|
||||
item.setId(IdWorker.getId()); item.setReturnOrderId(order.getId()); item.setCustomerId(customerId);
|
||||
item.setSkuCode(text(input.skuCode()));
|
||||
item.setProductNameZh(text(input.productNameZh())); item.setExpectedQuantity(input.expectedQuantity());
|
||||
item.setReceivedQuantity(0L); item.setCreatedAt(now); item.setUpdatedAt(now); orderItems.insert(item);
|
||||
}
|
||||
return detail(customerId, order.getReturnNo());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnOrderResponses.Summary> page(String customerId, ReturnOrderRequests.Query query) {
|
||||
LambdaQueryWrapper<ReturnOrder> w = new LambdaQueryWrapper<ReturnOrder>()
|
||||
.eq(ReturnOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.returnNo()), ReturnOrder::getReturnNo, text(query.returnNo()))
|
||||
.eq(StringUtils.hasText(query.status()), ReturnOrder::getStatus, norm(query.status()))
|
||||
.orderByDesc(ReturnOrder::getCreatedAt).orderByDesc(ReturnOrder::getId);
|
||||
if (StringUtils.hasText(query.trackingNo()) || StringUtils.hasText(query.skuCode())) {
|
||||
Set<Long> ids = null;
|
||||
if (StringUtils.hasText(query.skuCode())) {
|
||||
ids = new LinkedHashSet<>(orderItems.selectList(new LambdaQueryWrapper<ReturnOrderItem>()
|
||||
.eq(ReturnOrderItem::getCustomerId, customerId)
|
||||
.like(ReturnOrderItem::getSkuCode, text(query.skuCode())))
|
||||
.stream().map(ReturnOrderItem::getReturnOrderId).toList());
|
||||
}
|
||||
if (StringUtils.hasText(query.trackingNo())) {
|
||||
Set<Long> trackingIds = new LinkedHashSet<>(receipts.selectList(new LambdaQueryWrapper<ReturnReceipt>()
|
||||
.eq(ReturnReceipt::getCustomerId, customerId)
|
||||
.like(ReturnReceipt::getTrackingNo, text(query.trackingNo())))
|
||||
.stream().map(ReturnReceipt::getReturnOrderId).toList());
|
||||
if (ids == null) ids = trackingIds; else ids.retainAll(trackingIds);
|
||||
}
|
||||
if (ids.isEmpty()) return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
|
||||
w.in(ReturnOrder::getId, ids);
|
||||
}
|
||||
Page<ReturnOrder> result = orders.selectPage(Page.of(query.page(), query.size()), w);
|
||||
return new PageResult<>(result.getRecords().stream().map(this::summary).toList(), result.getCurrent(),
|
||||
result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnOrderResponses.Detail detail(String customerId, String returnNo) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货单不存在");
|
||||
List<ReturnOrderResponses.Item> items = orderItems.listOrder(order.getId()).stream().map(this::item).toList();
|
||||
List<ReturnOrderResponses.Receipt> receiptViews = receipts.listOrder(order.getId()).stream().map(receipt ->
|
||||
new ReturnOrderResponses.Receipt(receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getCartonMark(), receipt.getTrackingNo(),
|
||||
receipt.getBoundBy(), value(receipt.getReceivedQuantity()), receipt.getOperatorName(), receipt.getSignedAt())).toList();
|
||||
return new ReturnOrderResponses.Detail(summary(order), order.getRemark(), items, receiptViews);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PdaReturnReceiptResponses.Start start(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.Start request) {
|
||||
ReturnOrder order = activeOrder(String.valueOf(identity.overseasWarehouseId()), request.returnNo());
|
||||
return new PdaReturnReceiptResponses.Start(order.getReturnNo(), order.getStatus(), orderItems.listOrder(order.getId()).stream()
|
||||
.filter(item -> value(item.getReceivedQuantity()) < value(item.getExpectedQuantity())).map(this::pdaItem).toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PdaReturnReceiptResponses.SkuCandidates skuCandidates(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.SkuLookup request) {
|
||||
String skuCode = text(request.skuCode());
|
||||
List<ReturnOrderItem> matchingItems = orderItems.activeSkuCandidates(String.valueOf(identity.overseasWarehouseId()), skuCode);
|
||||
Map<Long, ReturnOrder> candidateOrders = new LinkedHashMap<>();
|
||||
if (!matchingItems.isEmpty()) {
|
||||
for (ReturnOrder order : orders.selectBatchIds(matchingItems.stream().map(ReturnOrderItem::getReturnOrderId).distinct().toList())) {
|
||||
candidateOrders.put(order.getId(), order);
|
||||
}
|
||||
}
|
||||
List<PdaReturnReceiptResponses.Candidate> candidates = matchingItems.stream().map(item -> {
|
||||
ReturnOrder order = candidateOrders.get(item.getReturnOrderId());
|
||||
if (order == null) throw new IllegalStateException("SKU 候选关联退货单不存在");
|
||||
return new PdaReturnReceiptResponses.Candidate(order.getReturnNo(), order.getCustomerId(), order.getCustomerReferenceNo(),
|
||||
order.getStatus(), value(item.getExpectedQuantity()), value(item.getReceivedQuantity()),
|
||||
value(order.getExpectedQuantity()), value(order.getReceivedQuantity()));
|
||||
}).toList();
|
||||
return new PdaReturnReceiptResponses.SkuCandidates(skuCode, candidates);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaReturnReceiptResponses.Confirmed confirm(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.Confirm request) {
|
||||
String key = text(request.idempotencyKey());
|
||||
String requestHash = hash(request);
|
||||
ReturnReceipt replay = receipts.findKey(identity.operatorKey(), key);
|
||||
if (replay != null) return replayReceipt(replay, requestHash);
|
||||
String startedBy = norm(request.startedBy());
|
||||
if (!Set.of("RETURN_NO", "SKU").contains(startedBy)) throw badRequest("startedBy 必须为 RETURN_NO 或 SKU");
|
||||
String trackingNo = text(request.trackingNo());
|
||||
ReturnOrder order = orders.lockNo(text(request.returnNo()));
|
||||
if (order == null || !String.valueOf(identity.overseasWarehouseId()).equals(order.getOverseasWarehouseId())) {
|
||||
throw notFound("当前绑定仓没有该退货单");
|
||||
}
|
||||
if (!Set.of(PENDING_RECEIPT, PARTIALLY_RECEIVED).contains(order.getStatus())) {
|
||||
throw conflict("仅待签收或签收中的退货单允许 PDA 签收");
|
||||
}
|
||||
List<ReturnOrderItem> lockedItems = orderItems.lockOrder(order.getId());
|
||||
Map<String, Long> receivedBySku = aggregate(request.items());
|
||||
List<Allocation> allocations = allocate(lockedItems, receivedBySku);
|
||||
long receiptQuantity = allocations.stream().mapToLong(Allocation::quantity).sum();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long receiptId = IdWorker.getId();
|
||||
ReturnReceipt receipt = new ReturnReceipt();
|
||||
receipt.setId(receiptId); receipt.setReturnOrderId(order.getId()); receipt.setCustomerId(order.getCustomerId());
|
||||
receipt.setReceiptNo(numbers.next("RETURN_RECEIPT", order.getOverseasWarehouseId(), receiptId));
|
||||
receipt.setReceiptBatchNo(numbers.next("RETURN_BATCH", order.getOverseasWarehouseId(), receiptId));
|
||||
receipt.setTrackingNo(trackingNo); receipt.setBoundBy(startedBy); receipt.setStatus("SIGNED");
|
||||
receipt.setReceivedQuantity(receiptQuantity); receipt.setOperatorId(identity.operatorKey());
|
||||
receipt.setOperatorName(identity.operatorName()); receipt.setIdempotencyKey(key); receipt.setRequestHash(requestHash);
|
||||
receipt.setSignedAt(now); receipt.setCreatedAt(now);
|
||||
try { receipts.insert(receipt); }
|
||||
catch (DuplicateKeyException concurrent) {
|
||||
ReturnReceipt existing = receipts.findKey(identity.operatorKey(), key);
|
||||
if (existing != null) return replayReceipt(existing, requestHash);
|
||||
throw concurrent;
|
||||
}
|
||||
for (Allocation allocation : allocations) {
|
||||
ReturnOrderItem item = allocation.item();
|
||||
item.setReceivedQuantity(value(item.getReceivedQuantity()) + allocation.quantity()); item.setUpdatedAt(now);
|
||||
orderItems.updateById(item);
|
||||
ReturnReceiptItem receiptItem = new ReturnReceiptItem();
|
||||
receiptItem.setId(IdWorker.getId()); receiptItem.setReceiptId(receiptId); receiptItem.setReturnOrderItemId(item.getId());
|
||||
receiptItem.setCustomerId(order.getCustomerId()); receiptItem.setSkuCode(item.getSkuCode());
|
||||
receiptItem.setReceivedQuantity(allocation.quantity()); receiptItems.insert(receiptItem);
|
||||
}
|
||||
Map<String, ReturnInventoryService.ReceiptLine> inventoryLines = new LinkedHashMap<>();
|
||||
for (Allocation allocation : allocations) {
|
||||
ReturnOrderItem item = allocation.item();
|
||||
inventoryLines.merge(item.getSkuCode(), new ReturnInventoryService.ReceiptLine(item.getSkuCode(),
|
||||
item.getProductNameZh(), allocation.quantity()), (left, right) -> new ReturnInventoryService.ReceiptLine(
|
||||
left.skuCode(), left.productNameZh() == null ? right.productNameZh() : left.productNameZh(),
|
||||
Math.addExact(left.quantity(), right.quantity())));
|
||||
}
|
||||
receipt.setCartonMark(inventory.createReceiptCarton(order, receipt, request.cartonMark(),
|
||||
new ArrayList<>(inventoryLines.values())).getCartonMark());
|
||||
receipts.updateById(receipt);
|
||||
List<ReturnOrderItem> finalItems = orderItems.listOrder(order.getId());
|
||||
long received = finalItems.stream().mapToLong(item -> value(item.getReceivedQuantity())).sum();
|
||||
boolean completed = finalItems.stream().allMatch(item -> value(item.getReceivedQuantity()) == value(item.getExpectedQuantity()));
|
||||
order.setReceivedQuantity(received); order.setStatus(completed ? COMPLETED : PARTIALLY_RECEIVED); order.setUpdatedAt(now); orders.updateById(order);
|
||||
ReturnReceiptBatch batch = new ReturnReceiptBatch();
|
||||
batch.setId(IdWorker.getId()); batch.setReturnOrderId(order.getId()); batch.setReceiptId(receiptId);
|
||||
batch.setCustomerId(order.getCustomerId()); batch.setBatchNo(receipt.getReceiptBatchNo());
|
||||
batch.setOverseasWarehouseId(order.getOverseasWarehouseId()); batch.setStorageBillingStatus("PENDING_PRICING");
|
||||
batch.setStorageStartedAt(now); batch.setCreatedAt(now); batches.insert(batch);
|
||||
return confirmed(order, receipt, completed);
|
||||
}
|
||||
|
||||
private ReturnOrder activeOrder(String warehouseId, String returnNo) {
|
||||
ReturnOrder order = orders.findActiveNo(warehouseId, text(returnNo));
|
||||
if (order == null) throw notFound("当前仓没有待签收或签收中的退货单");
|
||||
return order;
|
||||
}
|
||||
private List<Allocation> allocate(List<ReturnOrderItem> source, Map<String, Long> requested) {
|
||||
List<Allocation> allocations = new ArrayList<>();
|
||||
for (Map.Entry<String, Long> entry : requested.entrySet()) {
|
||||
long remaining = entry.getValue();
|
||||
for (ReturnOrderItem item : source) {
|
||||
if (!entry.getKey().equals(item.getSkuCode())) continue;
|
||||
long capacity = value(item.getExpectedQuantity()) - value(item.getReceivedQuantity());
|
||||
long accepted = Math.min(remaining, capacity);
|
||||
if (accepted > 0) { allocations.add(new Allocation(item, accepted)); remaining -= accepted; }
|
||||
if (remaining == 0) break;
|
||||
}
|
||||
if (remaining > 0) throw conflict("SKU 未预报或超出预报可签收数量: " + entry.getKey());
|
||||
}
|
||||
return allocations;
|
||||
}
|
||||
|
||||
private Map<String, Long> aggregate(List<PdaReturnReceiptRequests.Item> inputs) {
|
||||
Map<String, Long> result = new LinkedHashMap<>();
|
||||
for (PdaReturnReceiptRequests.Item item : inputs) result.merge(text(item.skuCode()), item.receivedQuantity(), Math::addExact);
|
||||
return result;
|
||||
}
|
||||
private ReturnOrderResponses.Detail replayOrder(String customerId, ReturnOrder order, String requestHash) {
|
||||
if (!requestHash.equals(order.getRequestHash())) throw conflict("幂等键已用于不同的退货预报");
|
||||
return detail(customerId, order.getReturnNo());
|
||||
}
|
||||
private PdaReturnReceiptResponses.Confirmed replayReceipt(ReturnReceipt receipt, String requestHash) {
|
||||
if (!requestHash.equals(receipt.getRequestHash())) throw conflict("幂等键已用于不同的签收请求");
|
||||
ReturnOrder order = orders.selectById(receipt.getReturnOrderId());
|
||||
if (order == null) throw new IllegalStateException("签收单关联退货单不存在");
|
||||
return confirmed(order, receipt, COMPLETED.equals(order.getStatus()));
|
||||
}
|
||||
private PdaReturnReceiptResponses.Confirmed confirmed(ReturnOrder order, ReturnReceipt receipt, boolean completed) {
|
||||
return new PdaReturnReceiptResponses.Confirmed(order.getReturnNo(), receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getCartonMark(),
|
||||
order.getStatus(), completed, value(order.getExpectedQuantity()), value(order.getReceivedQuantity()), receipt.getSignedAt());
|
||||
}
|
||||
private ReturnOrderResponses.Summary summary(ReturnOrder order) { return new ReturnOrderResponses.Summary(order.getReturnNo(),
|
||||
order.getOverseasWarehouseId(), order.getCustomerReferenceNo(), order.getStatus(),
|
||||
order.getExpectedSkuTypes() == null ? 0 : order.getExpectedSkuTypes(), value(order.getExpectedQuantity()),
|
||||
value(order.getReceivedQuantity()), order.getCreatedAt()); }
|
||||
private ReturnOrderResponses.Item item(ReturnOrderItem item) { return new ReturnOrderResponses.Item(item.getSkuCode(),
|
||||
item.getProductNameZh(), value(item.getExpectedQuantity()), value(item.getReceivedQuantity())); }
|
||||
private PdaReturnReceiptResponses.Item pdaItem(ReturnOrderItem item) { return new PdaReturnReceiptResponses.Item(item.getSkuCode(),
|
||||
item.getProductNameZh(), value(item.getExpectedQuantity()), value(item.getReceivedQuantity())); }
|
||||
private static long value(Long value) { return value == null ? 0 : value; }
|
||||
private static String text(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String norm(String value) { return text(value) == null ? null : text(value).toUpperCase(java.util.Locale.ROOT); }
|
||||
private String hash(Object value) { try { return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.writeValueAsString(value).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (Exception exception) { throw new IllegalStateException("无法生成请求哈希", exception); } }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
private record Allocation(ReturnOrderItem item, long quantity) { }
|
||||
}
|
||||
@@ -54,6 +54,10 @@ public class LastlegOrderNoGenerator {
|
||||
case "DISCARD" -> "CD";
|
||||
case "PROCUREMENT" -> "PR";
|
||||
case "INBOUND_PLAN" -> "IP";
|
||||
case "RETURN" -> "RT";
|
||||
case "RETURN_RECEIPT" -> "RR";
|
||||
case "RETURN_BATCH" -> "RB";
|
||||
case "RETURN_CARTON" -> "RC";
|
||||
default -> {
|
||||
String normalized = code.replaceAll("[^A-Z0-9]", "");
|
||||
yield normalized.isEmpty() ? "LMO" : normalized.substring(0, Math.min(4, normalized.length()));
|
||||
|
||||
Reference in New Issue
Block a user