修改提交
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user