修改提交

This commit is contained in:
2026-09-08 17:58:42 +08:00
parent f0939c91c3
commit 2a27e28f26
67 changed files with 2590 additions and 110 deletions
@@ -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;
}
@@ -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) { }
@@ -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,
@@ -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();
@@ -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)