贴标申请,仓储转运
This commit is contained in:
+11
-1
@@ -39,7 +39,17 @@ public class DeliveryCompletedOutboxService {
|
||||
Objects.requireNonNull(completedAt, "completedAt");
|
||||
Map<String, LastlegOrder> bySourceInbound = new LinkedHashMap<>();
|
||||
for (LastlegOrder order : orders == null ? List.<LastlegOrder>of() : orders) {
|
||||
if ("RELABEL_APPLICATION".equals(order.getOrderType())) continue;
|
||||
// Only orders projected from the domestic first-leg domain need an
|
||||
// acknowledgement event. Customer/local overseas orders can still carry
|
||||
// a local sourceInboundId, but deliberately have no domestic sourceOrderId.
|
||||
if (StringUtils.hasText(order.getSourceRegion())
|
||||
&& !RegionCode.CN.name().equalsIgnoreCase(order.getSourceRegion())) continue;
|
||||
// Relabel and warehouse-transfer deliveries are created and completed wholly
|
||||
// in the overseas last-leg domain. They do not have a domestic source order
|
||||
// to acknowledge, so publishing the cross-region event would make receipt
|
||||
// completion fail on the missing source-order fields.
|
||||
if ("RELABEL_APPLICATION".equals(order.getOrderType())
|
||||
|| "WAREHOUSE_TRANSFER".equals(order.getOrderType())) continue;
|
||||
if (!StringUtils.hasText(order.getSourceInboundId())) continue;
|
||||
requireText(order.getSourceOrderId(), "sourceOrderId");
|
||||
requireText(order.getInboundCode(), "inboundCode");
|
||||
|
||||
+18
-6
@@ -1,24 +1,36 @@
|
||||
package com.baiyue.lastleg.palletops.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class PalletOperationResponses {
|
||||
private PalletOperationResponses() { }
|
||||
public record RelabelSource(long positionId, long positionVersion, String palletCode,
|
||||
public record RelabelSource(
|
||||
@JsonSerialize(using = ToStringSerializer.class) long positionId,
|
||||
long positionVersion,
|
||||
@JsonSerialize(using = ToStringSerializer.class) long palletId, String palletCode,
|
||||
String locationCode, String inboundCode, long stockPackages,
|
||||
long movablePackages, List<String> cartonMarks) { }
|
||||
|
||||
public record InventoryLine(String customerId, String customerName, long balanceId, long positionId,
|
||||
public record InventoryLine(String customerId, String customerName,
|
||||
@JsonSerialize(using = ToStringSerializer.class) long balanceId,
|
||||
@JsonSerialize(using = ToStringSerializer.class) long positionId,
|
||||
String inboundCode, String skuCode, String productName,
|
||||
long availablePackages, long holdPackages, long allocatedPackages,
|
||||
long pickedPackages, long receivingPackages, long positionVersion) { }
|
||||
public record PalletContent(long palletId, String palletCode, String status, int palletVersion,
|
||||
Long locationId, String locationCode, String destinationWarehouseCode,
|
||||
public record PalletContent(@JsonSerialize(using = ToStringSerializer.class) long palletId,
|
||||
String palletCode, String status, int palletVersion,
|
||||
@JsonSerialize(using = ToStringSerializer.class) Long locationId,
|
||||
String locationCode, String destinationWarehouseCode,
|
||||
long totalPackages, List<InventoryLine> inventory) { }
|
||||
public record PrintablePallet(long palletId, String palletCode, int palletVersion,
|
||||
public record PrintablePallet(@JsonSerialize(using = ToStringSerializer.class) long palletId,
|
||||
String palletCode, int palletVersion,
|
||||
String relabelNo, String printableHtml) { }
|
||||
public record Operation(long operationId, String operationType, String idempotencyKey,
|
||||
public record Operation(@JsonSerialize(using = ToStringSerializer.class) long operationId,
|
||||
String operationType, String idempotencyKey,
|
||||
String status, boolean replayed, LocalDateTime completedAt,
|
||||
List<PalletContent> pallets) {
|
||||
public Operation asReplay() {
|
||||
|
||||
+12
-1
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.palletops.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -30,7 +31,14 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
|
||||
|
||||
@Override
|
||||
public PalletReference ensureRelabelPallet(long warehouseId, String businessId) {
|
||||
Map<String, Object> body = Map.of("warehouseId", warehouseId, "businessId", businessId);
|
||||
return ensureRelabelPallet(warehouseId, businessId, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PalletReference ensureRelabelPallet(long warehouseId, String businessId, String fbaWarehouseCode) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("warehouseId", warehouseId); body.put("businessId", businessId);
|
||||
if (fbaWarehouseCode != null && !fbaWarehouseCode.isBlank()) body.put("fbaWarehouseCode", fbaWarehouseCode.trim());
|
||||
ResolveEnvelope response = client.post().uri("/internal/v1/warehouse-pallets/ensure-relabel")
|
||||
.header("X-Internal-Service-Token", internalToken).body(body).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
@@ -117,7 +125,10 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway {
|
||||
}).toBodilessEntity();
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record ResolveEnvelope(String code, String message, PalletReference data) { }
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record LocationEnvelope(LocationReference data) { }
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record TransitionEnvelope(Transition data) { }
|
||||
}
|
||||
|
||||
+6
@@ -1,7 +1,12 @@
|
||||
package com.baiyue.lastleg.palletops.gateway;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
public interface PalletPhysicalGateway {
|
||||
PalletReference ensureRelabelPallet(long warehouseId, String businessId);
|
||||
default PalletReference ensureRelabelPallet(long warehouseId, String businessId, String fbaWarehouseCode) {
|
||||
return ensureRelabelPallet(warehouseId, businessId);
|
||||
}
|
||||
PalletReference resolve(String palletCode);
|
||||
PalletReference resolve(long palletId);
|
||||
LocationReference resolveLocation(long warehouseId, long locationId);
|
||||
@@ -9,6 +14,7 @@ public interface PalletPhysicalGateway {
|
||||
String operationKey, String reason);
|
||||
void compensate(long warehouseId, Transition transition);
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
record PalletReference(long palletId, String palletCode, long warehouseId, Long locationId,
|
||||
String status, String fbaWarehouseCode, java.math.BigDecimal unitPrice, int version) {
|
||||
public PalletReference(long palletId, String palletCode, long warehouseId, Long locationId,
|
||||
|
||||
+168
-9
@@ -18,6 +18,11 @@ import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelPalletMutationService;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -48,6 +53,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PdaPalletOperationService {
|
||||
@@ -61,6 +67,9 @@ public class PdaPalletOperationService {
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelPalletMutationService relabelPalletMutations;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private ReturnInventoryBalanceMapper returnBalances;
|
||||
private TransactionTemplate transaction;
|
||||
private TransactionTemplate newTransaction;
|
||||
|
||||
@@ -85,6 +94,26 @@ public class PdaPalletOperationService {
|
||||
Set<String> boundMarks = boundRelabelMarks(app);
|
||||
List<RelabelItem> items = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId()));
|
||||
if (!items.isEmpty() && items.stream().allMatch(item -> item.getReturnCartonId() != null)) {
|
||||
List<PalletOperationResponses.RelabelSource> result = new ArrayList<>();
|
||||
Map<Long, List<RelabelItem>> byCarton = items.stream().filter(item -> !boundMarks.contains(item.getCartonMark())).collect(Collectors.groupingBy(
|
||||
RelabelItem::getReturnCartonId, LinkedHashMap::new, Collectors.toList()));
|
||||
for (var entry : byCarton.entrySet()) {
|
||||
ReturnInventoryCarton carton = returnCartons.selectById(entry.getKey());
|
||||
if (carton == null || !Objects.equals(carton.getCustomerId(), app.getCustomerId())
|
||||
|| !Objects.equals(carton.getOverseasWarehouseId(), app.getOverseasWarehouseId())
|
||||
|| !Objects.equals(carton.getStatus(), "RELABEL_PENDING"))
|
||||
throw conflict("退货换标来源托盘不存在或已被占用");
|
||||
PalletPhysicalGateway.PalletReference pallet = physical.resolve(carton.getPalletCode());
|
||||
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId())
|
||||
throw conflict("退货换标来源托盘不属于当前仓库");
|
||||
long quantity = value(carton.getTotalQuantity());
|
||||
result.add(new PalletOperationResponses.RelabelSource(carton.getId(), 0L,
|
||||
pallet.palletId(), pallet.palletCode(), null, carton.getSourceBatchNo(), quantity, quantity,
|
||||
entry.getValue().stream().map(RelabelItem::getCartonMark).filter(Objects::nonNull).toList()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Map<Long, List<RelabelItem>> byBalance = new LinkedHashMap<>();
|
||||
for (RelabelItem item : items) {
|
||||
InventoryPosition anchor = positions.selectById(item.getSourceInventoryPositionId());
|
||||
@@ -99,7 +128,7 @@ public class PdaPalletOperationService {
|
||||
long ownHold = group.stream().filter(item -> Objects.equals(item.getSourceInventoryPositionId(), row.getPositionId()))
|
||||
.mapToLong(item -> value(item.getNotifiedPackageCount())).sum();
|
||||
result.add(new PalletOperationResponses.RelabelSource(row.getPositionId(), value(row.getVersion()),
|
||||
row.getPalletNo(), row.getWarehouseLocationCode(), row.getInboundCode(),
|
||||
row.getPalletId(), row.getPalletNo(), row.getWarehouseLocationCode(), row.getInboundCode(),
|
||||
value(row.getAvailablePackages()) + value(row.getHoldPackages()) + value(row.getAllocatedPackages())
|
||||
+ value(row.getPickedPackages()),
|
||||
value(row.getAvailablePackages()) + Math.min(ownHold, value(row.getHoldPackages())),
|
||||
@@ -118,15 +147,22 @@ public class PdaPalletOperationService {
|
||||
throw conflict("换标单不属于 Token 冻结的海外仓");
|
||||
}
|
||||
String key = required(request.idempotencyKey(), "幂等键不能为空");
|
||||
String destination = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", application.getId()).eq("customer_id", application.getCustomerId()))
|
||||
.stream().map(RelabelItem::getDestinationFbaWarehouseCode).filter(value -> value != null && !value.isBlank())
|
||||
.distinct().reduce((left, right) -> { throw conflict("同一换标单只能绑定一个 FBA 目的仓"); }).orElse(null);
|
||||
PalletPhysicalGateway.PalletReference pallet = physical.ensureRelabelPallet(identity.overseasWarehouseId(),
|
||||
relabelNo + ":" + key);
|
||||
relabelNo + ":" + key, destination);
|
||||
String code = HtmlUtils.htmlEscape(pallet.palletCode());
|
||||
String order = HtmlUtils.htmlEscape(relabelNo);
|
||||
String fbaValue = destination == null ? pallet.fbaWarehouseCode() : destination;
|
||||
String fba = HtmlUtils.htmlEscape(fbaValue == null ? "" : fbaValue);
|
||||
if (fba == null || fba.isBlank()) fba = "待绑定";
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code
|
||||
+ "</title><style>@page{size:100mm 70mm;margin:4mm}body{font-family:Arial,sans-serif;text-align:center;margin:0}"
|
||||
+ "h1{font-size:24px;margin:8mm 0 4mm;letter-spacing:0}.code{font-size:18px;font-weight:700;word-break:break-all}"
|
||||
+ ".ref{font-size:12px;margin-top:6mm}</style></head><body><h1>托盘标签</h1><div class=\"code\">"
|
||||
+ code + "</div><div class=\"ref\">换标单:" + order + "</div></body></html>";
|
||||
+ code + "</div><div class=\"ref\">换标单:" + order + "<br>FBA目的仓:" + fba + "</div></body></html>";
|
||||
return new PalletOperationResponses.PrintablePallet(pallet.palletId(), pallet.palletCode(), pallet.version(),
|
||||
relabelNo, html);
|
||||
}
|
||||
@@ -205,8 +241,10 @@ public class PdaPalletOperationService {
|
||||
Map<Long, Long> packagesByPosition = new LinkedHashMap<>();
|
||||
Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>();
|
||||
Map<Long, InventoryPosition> anchors = new LinkedHashMap<>();
|
||||
InventoryPosition actual = request.actualSource() == null ? null : positions.selectById(request.actualSource().positionId());
|
||||
if (request.actualSource() != null && (actual == null
|
||||
boolean returnRelabel = selected.stream().allMatch(item -> item.getReturnCartonId() != null);
|
||||
InventoryPosition actual = returnRelabel || request.actualSource() == null
|
||||
? null : positions.selectById(request.actualSource().positionId());
|
||||
if (!returnRelabel && request.actualSource() != null && (actual == null
|
||||
|| !Objects.equals(actual.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(actual.getPalletId(), source.palletId())
|
||||
|| !Objects.equals(actual.getOverseasWarehouseId(), application.getOverseasWarehouseId())
|
||||
@@ -219,13 +257,17 @@ public class PdaPalletOperationService {
|
||||
if ("PROCESSING".equals(application.getStatus())) {
|
||||
var workTask = relabelWorkTasks.find(application.getCustomerId(), application.getId());
|
||||
for (RelabelItem item : selected) {
|
||||
Long completed = relabelWorkFacts.selectCount(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact>()
|
||||
.eq("task_id", workTask.getId()).eq("customer_id", application.getCustomerId())
|
||||
.eq("item_id", item.getId()).eq("result", "RELABELLED"));
|
||||
if (completed == null || completed < value(item.getNotifiedPackageCount()))
|
||||
long completed = relabelWorkFacts.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact>()
|
||||
.eq("task_id", workTask.getId()).eq("customer_id", application.getCustomerId())
|
||||
.eq("item_id", item.getId()).eq("result", "RELABELLED"))
|
||||
.stream().mapToLong(fact -> fact.getQuantity() == null ? 1L : fact.getQuantity()).sum();
|
||||
if (completed < value(item.getNotifiedPackageCount()))
|
||||
throw conflict("请先完成本箱全部换标作业,再进行组托");
|
||||
}
|
||||
}
|
||||
if (returnRelabel) {
|
||||
return bindReturnRelabelPallet(identity, application, selected, source, target, request);
|
||||
}
|
||||
for (RelabelItem item : selected) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
|
||||
|| item.getNotifiedPackageCount() <= 0) throw conflict("换标箱唛缺少库存位置或箱数快照");
|
||||
@@ -265,6 +307,107 @@ public class PdaPalletOperationService {
|
||||
reason, relabelPlan);
|
||||
}
|
||||
|
||||
private PalletOperationResponses.Operation bindReturnRelabelPallet(PdaWarehouseIdentity identity,
|
||||
RelabelApplication application, List<RelabelItem> selected,
|
||||
PalletPhysicalGateway.PalletReference source, PalletPhysicalGateway.PalletReference target,
|
||||
PalletOperationRequests.RelabelDisassemble request) {
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
|
||||
Set<String> alreadyBound = boundRelabelMarks(application);
|
||||
if (selected.stream().map(RelabelItem::getCartonMark).anyMatch(alreadyBound::contains))
|
||||
throw conflict("退货换标箱已在本轮完成组托,请刷新后重试");
|
||||
long quantity = 0;
|
||||
List<ReturnInventoryCarton> cartons = new ArrayList<>();
|
||||
for (RelabelItem item : selected) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(carton.getPalletCode(), source.palletCode())
|
||||
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|
||||
|| !Objects.equals(carton.getStatus(), "RELABEL_PENDING"))
|
||||
throw conflict("退货换标来源托盘或库存状态已变化,请刷新后重试");
|
||||
quantity += value(item.getNotifiedPackageCount());
|
||||
cartons.add(carton);
|
||||
}
|
||||
if (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|
||||
|| request.actualSource().packages() != quantity)
|
||||
throw conflict("退货换标拆出数量必须等于本批已完成贴标数量");
|
||||
String key = required(request.idempotencyKey(), "幂等键不能为空");
|
||||
String requestHash = hash(request);
|
||||
PdaPalletOperation operation = begin(identity, "RELABEL_PALLET_DISASSEMBLE", key, requestHash);
|
||||
if ("COMPLETED".equals(operation.getStatus())) return read(operation).asReplay();
|
||||
physical.apply(identity.overseasWarehouseId(), target, "OCCUPIED", target.locationId(), key, "退货换标组托");
|
||||
return transaction.execute(status -> {
|
||||
for (int index = 0; index < cartons.size(); index++) {
|
||||
RelabelItem item = selected.get(index);
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(carton.getPalletCode(), source.palletCode())
|
||||
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|
||||
|| !"RELABEL_PENDING".equals(carton.getStatus()))
|
||||
throw conflict("退货换标来源库存已变化,请刷新后重试");
|
||||
long move = value(item.getNotifiedPackageCount());
|
||||
if (move <= 0 || move > value(carton.getTotalQuantity())) throw conflict("来源托盘可拆数量不足");
|
||||
if (move < value(carton.getTotalQuantity())) {
|
||||
splitReturnRelabelCarton(application, item, carton, target, move);
|
||||
} else {
|
||||
carton.setPalletCode(target.palletCode());
|
||||
carton.setDestinationWarehouseCode(item.getDestinationFbaWarehouseCode());
|
||||
carton.setRelabelStatus("PROCESSING");
|
||||
carton.setUpdatedAt(LocalDateTime.now());
|
||||
returnCartons.updateById(carton);
|
||||
}
|
||||
}
|
||||
recordReturnRelabelBinding(identity, application, selected, target.palletCode());
|
||||
List<PalletOperationResponses.PalletContent> contents = List.of(
|
||||
content(identity.overseasWarehouseId(), physical.resolve(source.palletCode())),
|
||||
content(identity.overseasWarehouseId(), physical.resolve(target.palletCode())));
|
||||
PalletOperationResponses.Operation result = new PalletOperationResponses.Operation(operation.getId(),
|
||||
operation.getOperationType(), key, "COMPLETED", false, LocalDateTime.now(), contents);
|
||||
operation.setStatus("COMPLETED"); operation.setResultJson(write(result));
|
||||
operation.setUpdatedAt(result.completedAt()); mapper.updateById(operation);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/** Keep the unlabelled remainder on its original pallet; move only the confirmed SKU pieces. */
|
||||
private void splitReturnRelabelCarton(RelabelApplication application, RelabelItem item,
|
||||
ReturnInventoryCarton source, PalletPhysicalGateway.PalletReference target, long quantity) {
|
||||
if (relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", application.getId()).eq("return_carton_id", source.getId())).size() != 1)
|
||||
throw conflict("来源托盘包含多项换标任务,不能按单 SKU 拆分,请核对库存");
|
||||
List<ReturnInventoryCartonItem> lines = returnCartonItems.lockCarton(source.getId());
|
||||
if (lines.size() != 1 || !item.getSkuCode().equalsIgnoreCase(lines.get(0).getSkuCode())
|
||||
|| value(lines.get(0).getQuantity()) != value(source.getTotalQuantity()))
|
||||
throw conflict("来源托盘包含其他 SKU,不能按单 SKU 拆分,请核对库存");
|
||||
long remaining = value(source.getTotalQuantity()) - quantity;
|
||||
ReturnInventoryCartonItem sourceLine = lines.get(0);
|
||||
sourceLine.setQuantity(remaining);
|
||||
if (returnCartonItems.updateById(sourceLine) != 1
|
||||
|| returnCartons.releasePartial(source.getId(), application.getCustomerId(), application.getId(), remaining) != 1)
|
||||
throw conflict("来源托盘数量已变化,请刷新后重试");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long targetId = IdWorker.getId();
|
||||
ReturnInventoryCarton moved = new ReturnInventoryCarton();
|
||||
moved.setId(targetId); moved.setCustomerId(source.getCustomerId());
|
||||
moved.setOverseasWarehouseId(source.getOverseasWarehouseId());
|
||||
moved.setSourceBatchNo(source.getSourceBatchNo()); moved.setSourceReturnNo(source.getSourceReturnNo());
|
||||
moved.setPalletCode(target.palletCode()); moved.setCartonMark("RL-CARTON-" + targetId);
|
||||
moved.setDestinationWarehouseCode(item.getDestinationFbaWarehouseCode());
|
||||
moved.setRelabelApplicationId(application.getId()); moved.setRelabelNo(application.getRelabelNo());
|
||||
moved.setRelabelStatus("PROCESSING"); moved.setStatus("RELABEL_PENDING");
|
||||
moved.setSkuTypes(1); moved.setTotalQuantity(quantity);
|
||||
moved.setCreatedAt(now); moved.setUpdatedAt(now);
|
||||
if (returnCartons.insert(moved) != 1) throw conflict("新托盘库存创建失败");
|
||||
ReturnInventoryCartonItem targetLine = new ReturnInventoryCartonItem();
|
||||
targetLine.setId(IdWorker.getId()); targetLine.setCartonId(targetId);
|
||||
targetLine.setCustomerId(source.getCustomerId()); targetLine.setSkuCode(sourceLine.getSkuCode());
|
||||
targetLine.setProductNameZh(sourceLine.getProductNameZh()); targetLine.setQuantity(quantity);
|
||||
if (returnCartonItems.insert(targetLine) != 1
|
||||
|| returnBalances.adjustCartonQuantity(source.getCustomerId(), source.getOverseasWarehouseId(),
|
||||
sourceLine.getSkuCode(), 1) != 1) throw conflict("退货库存拆分记账失败");
|
||||
item.setReturnCartonId(targetId);
|
||||
if (relabelItems.updateById(item) != 1) throw conflict("换标单来源托盘更新失败");
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Swap request) {
|
||||
PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_SWAP",
|
||||
@@ -610,6 +753,22 @@ public class PdaPalletOperationService {
|
||||
relabelWorkHistory.insert(record);
|
||||
}
|
||||
|
||||
private void recordReturnRelabelBinding(PdaWarehouseIdentity identity, RelabelApplication app,
|
||||
List<RelabelItem> items, String targetPallet) {
|
||||
var task = relabelWorkTasks.find(app.getCustomerId(), app.getId());
|
||||
if (task == null) throw conflict("退货换标作业任务不存在,不能记录组托");
|
||||
var record = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
|
||||
record.setId(IdWorker.getId()); record.setTaskId(task.getId()); record.setApplicationId(app.getId());
|
||||
record.setCustomerId(app.getCustomerId()); record.setAction("PALLET_BIND");
|
||||
record.setFromStatus(task.getStatus()); record.setToStatus(task.getStatus());
|
||||
record.setOperatorKey(identity.operatorKey()); record.setOperatorName(identity.operatorName());
|
||||
record.setCreatedAt(LocalDateTime.now());
|
||||
record.setDetailJson(write(Map.of("round", Objects.toString(task.getReviewedAt(), "INITIAL"),
|
||||
"cartonMarks", items.stream().map(RelabelItem::getCartonMark).toList(),
|
||||
"targetPallets", List.of(targetPallet))));
|
||||
relabelWorkHistory.insert(record);
|
||||
}
|
||||
|
||||
private void requireRelabelWork(PdaWarehouseIdentity identity, RelabelApplication application) {
|
||||
if (application == null || !Set.of("PROCESSING", "READY_FOR_OUTBOUND").contains(application.getStatus()))
|
||||
throw conflict("当前换标状态不能组托");
|
||||
|
||||
+16
@@ -78,6 +78,22 @@ public class PdaRelabelController {
|
||||
identity.overseasWarehouseId(), relabelNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{relabelNo}/return-skus/complete")
|
||||
public ApiResponse<PdaRelabelResponses.Detail> completeReturnSku(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @Valid @RequestBody PdaRelabelRequests.BulkSku request) {
|
||||
Identity identity = identity(user);
|
||||
return ApiResponse.success(service.completeReturnSku(identity.operatorKey(), identity.operatorName(),
|
||||
identity.overseasWarehouseId(), relabelNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{relabelNo}/return-skus/complete-total")
|
||||
public ApiResponse<PdaRelabelResponses.Detail> completeReturnSkuTotal(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @Valid @RequestBody PdaRelabelRequests.BulkSkuTotal request) {
|
||||
Identity identity = identity(user);
|
||||
return ApiResponse.success(service.completeReturnSkuTotal(identity.operatorKey(), identity.operatorName(),
|
||||
identity.overseasWarehouseId(), relabelNo, request));
|
||||
}
|
||||
|
||||
@GetMapping("/{relabelNo}/documents")
|
||||
public ApiResponse<List<PdaRelabelService.Document>> documents(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo) {
|
||||
|
||||
+10
@@ -37,6 +37,16 @@ public final class PdaRelabelRequests {
|
||||
public record SkuVerification(@NotBlank String skuCode, @NotBlank String newSkuCode,
|
||||
@Positive int quantity) { }
|
||||
|
||||
public record BulkSku(@NotNull @Positive Long itemId,
|
||||
@Positive int quantity,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record BulkSkuTotal(@NotBlank @Size(max = 255) String skuCode,
|
||||
@Positive int quantity,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record Skip(@NotNull @Positive Long itemId,
|
||||
@NotBlank @Size(max = 255) String oldLabel,
|
||||
@NotBlank @Size(max = 1000) String reason,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public final class PdaRelabelResponses {
|
||||
|
||||
public record UnitFact(Long factId, Long itemId, String oldLabel, String newLabel,
|
||||
String result, String exceptionCode, String reason,
|
||||
String operatorName, LocalDateTime occurredAt) { }
|
||||
int quantity, String operatorName, LocalDateTime occurredAt) { }
|
||||
|
||||
public record History(String action, String fromStatus, String toStatus,
|
||||
String operatorName, String detail, LocalDateTime createdAt) { }
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ public class PdaRelabelUnitFact {
|
||||
private String result;
|
||||
private String exceptionCode;
|
||||
private String reason;
|
||||
private Integer quantity;
|
||||
private String operatorKey;
|
||||
private String operatorName;
|
||||
private LocalDateTime occurredAt;
|
||||
|
||||
+12
@@ -90,6 +90,17 @@ public class HttpRelabelPhotoFileGateway implements RelabelPhotoFileGateway {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UploadedFile> list(String customerId, long taskId) {
|
||||
MetadataListEnvelope response = client.get()
|
||||
.uri("/internal/file/v1/business-files/{tenantId}/{businessType}/{businessId}", tenantId(customerId), BUSINESS_TYPE, taskId)
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Relabel photo list failed"); })
|
||||
.body(MetadataListEnvelope.class);
|
||||
if (response == null || !"200".equals(response.code()) || response.data() == null) return java.util.List.of();
|
||||
return response.data().stream().map(value -> new UploadedFile(value.fileId(), value.fileName(), value.contentType(), value.sizeBytes() == null ? 0L : value.sizeBytes())).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadedFile download(String customerId, long taskId, long fileId) {
|
||||
validate(customerId, taskId, fileId);
|
||||
@@ -130,6 +141,7 @@ public class HttpRelabelPhotoFileGateway implements RelabelPhotoFileGateway {
|
||||
}
|
||||
|
||||
private record MetadataEnvelope(String code, String message, Metadata data) { }
|
||||
private record MetadataListEnvelope(String code, String message, java.util.List<Metadata> data) { }
|
||||
private record Metadata(Long fileId, Long tenantId, String businessType, String businessId,
|
||||
String fileName, String contentType, Long sizeBytes) { }
|
||||
}
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ public interface RelabelPhotoFileGateway {
|
||||
UploadedFile upload(String customerId, long taskId, long operatorId, MultipartFile file);
|
||||
void validate(String customerId, long taskId, long fileId);
|
||||
DownloadedFile download(String customerId, long taskId, long fileId);
|
||||
java.util.List<UploadedFile> list(String customerId, long taskId);
|
||||
|
||||
record UploadedFile(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
record DownloadedFile(byte[] content, String fileName, String contentType) { }
|
||||
|
||||
+104
-8
@@ -39,6 +39,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -238,6 +239,88 @@ public class PdaRelabelService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaRelabelResponses.Detail completeReturnSku(String operatorKey, String operatorName, long warehouseId,
|
||||
String relabelNo, PdaRelabelRequests.BulkSku request) {
|
||||
UnitCommand command = new UnitCommand(relabelNo, "RETURN_BULK_SKU", request);
|
||||
PdaRelabelResponses.Detail replay = replay(operatorKey, warehouseId, relabelNo, "RETURN_BULK_SKU",
|
||||
request.idempotencyKey(), command);
|
||||
if (replay != null) return replay;
|
||||
RelabelApplication application = requireApplication(relabelNo, warehouseId, true);
|
||||
PdaRelabelTask task = requireOwnedTask(application, operatorKey, true);
|
||||
requireVersion(task, request.expectedVersion());
|
||||
RelabelItem item = requireItem(application, request.itemId());
|
||||
if (!"RETURN_BULK_SKU_LABEL".equals(operationType(item))) throw conflict("当前明细不是退货按件贴标任务");
|
||||
int completed = quantity(factList(task).stream().filter(fact -> item.getId().equals(fact.getItemId())).toList(), "RELABELLED");
|
||||
int remaining = value(item.getNotifiedPackageCount()) - completed;
|
||||
if (request.quantity() > remaining) throw conflict("本次完成数量超过该 SKU 剩余贴标数量");
|
||||
PdaRelabelUnitFact fact = new PdaRelabelUnitFact();
|
||||
fact.setId(IdWorker.getId()); fact.setTaskId(task.getId()); fact.setApplicationId(application.getId());
|
||||
// A bulk SKU is shared by many units/pallets; old_label is uniquely indexed for physical carton scans.
|
||||
fact.setItemId(item.getId()); fact.setCustomerId(application.getCustomerId());
|
||||
fact.setResult("RELABELLED"); fact.setQuantity(request.quantity()); fact.setOperatorKey(operatorKey);
|
||||
fact.setOperatorName(operatorName); fact.setOccurredAt(LocalDateTime.now()); unitFacts.insert(fact);
|
||||
task.setRelabelledPackages(value(task.getRelabelledPackages()) + request.quantity());
|
||||
updateTask(task, request.expectedVersion());
|
||||
insertHistory(task, "RETURN_BULK_SKU", PROCESSING, PROCESSING, operatorKey, operatorName,
|
||||
Map.of("factId", fact.getId(), "itemId", item.getId(), "skuCode", item.getSkuCode(), "quantity", request.quantity()));
|
||||
PdaRelabelResponses.Detail result = detail(application, task);
|
||||
saveOperation(task, operatorKey, "RETURN_BULK_SKU", request.idempotencyKey(), command, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaRelabelResponses.Detail completeReturnSkuTotal(String operatorKey, String operatorName, long warehouseId,
|
||||
String relabelNo, PdaRelabelRequests.BulkSkuTotal request) {
|
||||
UnitCommand command = new UnitCommand(relabelNo, "RETURN_BULK_SKU_TOTAL", request);
|
||||
PdaRelabelResponses.Detail replay = replay(operatorKey, warehouseId, relabelNo, "RETURN_BULK_SKU_TOTAL",
|
||||
request.idempotencyKey(), command);
|
||||
if (replay != null) return replay;
|
||||
RelabelApplication application = requireApplication(relabelNo, warehouseId, true);
|
||||
PdaRelabelTask task = requireOwnedTask(application, operatorKey, true);
|
||||
requireVersion(task, request.expectedVersion());
|
||||
String skuCode = request.skuCode().trim();
|
||||
List<RelabelItem> skuItems = itemList(application).stream()
|
||||
.filter(item -> "RETURN_BULK_SKU_LABEL".equals(operationType(item)))
|
||||
.filter(item -> skuCode.equalsIgnoreCase(Objects.toString(item.getSkuCode(), "").trim()))
|
||||
.toList();
|
||||
if (skuItems.isEmpty()) throw notFound("当前换标单没有该退货 SKU");
|
||||
List<PdaRelabelUnitFact> existingFacts = factList(task);
|
||||
Map<Long, Integer> completedByItem = new HashMap<>();
|
||||
for (RelabelItem item : skuItems) {
|
||||
completedByItem.put(item.getId(), quantity(existingFacts.stream()
|
||||
.filter(fact -> item.getId().equals(fact.getItemId())).toList(), "RELABELLED"));
|
||||
}
|
||||
int totalRemaining = skuItems.stream().mapToInt(item -> value(item.getNotifiedPackageCount())
|
||||
- completedByItem.getOrDefault(item.getId(), 0)).sum();
|
||||
if (request.quantity() > totalRemaining) throw conflict("本次完成数量超过该 SKU 剩余贴标数量");
|
||||
int remainingToAllocate = request.quantity();
|
||||
List<Map<String, Object>> allocations = new ArrayList<>();
|
||||
LocalDateTime occurredAt = LocalDateTime.now();
|
||||
for (RelabelItem item : skuItems) {
|
||||
if (remainingToAllocate == 0) break;
|
||||
int itemRemaining = value(item.getNotifiedPackageCount()) - completedByItem.getOrDefault(item.getId(), 0);
|
||||
int allocated = Math.min(remainingToAllocate, itemRemaining);
|
||||
if (allocated <= 0) continue;
|
||||
PdaRelabelUnitFact fact = new PdaRelabelUnitFact();
|
||||
fact.setId(IdWorker.getId()); fact.setTaskId(task.getId()); fact.setApplicationId(application.getId());
|
||||
// Do not write a shared SKU into the unique physical old-label column.
|
||||
fact.setItemId(item.getId()); fact.setCustomerId(application.getCustomerId());
|
||||
fact.setResult("RELABELLED"); fact.setQuantity(allocated); fact.setOperatorKey(operatorKey);
|
||||
fact.setOperatorName(operatorName); fact.setOccurredAt(occurredAt); unitFacts.insert(fact);
|
||||
allocations.add(Map.of("itemId", item.getId(), "cartonMark", Objects.toString(item.getCartonMark(), ""),
|
||||
"quantity", allocated, "factId", fact.getId()));
|
||||
remainingToAllocate -= allocated;
|
||||
}
|
||||
task.setRelabelledPackages(value(task.getRelabelledPackages()) + request.quantity());
|
||||
updateTask(task, request.expectedVersion());
|
||||
insertHistory(task, "RETURN_BULK_SKU_TOTAL", PROCESSING, PROCESSING, operatorKey, operatorName,
|
||||
Map.of("skuCode", skuCode, "quantity", request.quantity(), "allocations", allocations));
|
||||
PdaRelabelResponses.Detail result = detail(application, task);
|
||||
saveOperation(task, operatorKey, "RETURN_BULK_SKU_TOTAL", request.idempotencyKey(), command, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Document> documents(long warehouseId, String relabelNo) {
|
||||
RelabelApplication app = requireApplication(relabelNo, warehouseId, false);
|
||||
@@ -311,26 +394,28 @@ public class PdaRelabelService {
|
||||
if (!request.manualCompletion()) bindingGuard.requireBound(application);
|
||||
List<RelabelItem> detailItems = itemList(application);
|
||||
List<PdaRelabelUnitFact> facts = factList(task);
|
||||
boolean incomplete = detailItems.stream().anyMatch(item -> facts.stream()
|
||||
.filter(fact -> item.getId().equals(fact.getItemId())).count() < value(item.getNotifiedPackageCount()));
|
||||
boolean incomplete = detailItems.stream().anyMatch(item -> processedQuantity(facts, item.getId()) < value(item.getNotifiedPackageCount()));
|
||||
if ((incomplete || request.manualCompletion()) && !StringUtils.hasText(request.remark()))
|
||||
throw conflict("手动完成必须填写仓库异常作业备注");
|
||||
if (incomplete && !request.manualCompletion()) throw conflict("仍有未处理箱,请选择手动完成并填写作业备注");
|
||||
if (request.photoFileIds() == null || request.photoFileIds().isEmpty() || request.photoFileIds().size() > 20) throw conflict("完成换标必须上传 1 至 20 张作业照片");
|
||||
if (request.manualCompletion() && request.remark().trim().length() > 900) throw conflict("手动作业备注不能超过 900 字");
|
||||
for (RelabelItem item : detailItems) {
|
||||
long actual = facts.stream().filter(fact -> item.getId().equals(fact.getItemId())).count();
|
||||
long actual = processedQuantity(facts, item.getId());
|
||||
if (actual > value(item.getNotifiedPackageCount())) {
|
||||
throw conflict("Every relabel item must have exactly one fact per notified package");
|
||||
}
|
||||
for (long i = actual; i < value(item.getNotifiedPackageCount()); i++) {
|
||||
long missing = value(item.getNotifiedPackageCount()) - actual;
|
||||
int factCount = "RETURN_BULK_SKU_LABEL".equals(operationType(item)) && missing > 0 ? 1 : Math.toIntExact(missing);
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
PdaRelabelUnitFact fact = new PdaRelabelUnitFact();
|
||||
fact.setId(IdWorker.getId()); fact.setTaskId(task.getId()); fact.setApplicationId(application.getId());
|
||||
fact.setItemId(item.getId()); fact.setCustomerId(application.getCustomerId());
|
||||
fact.setResult("EXCEPTION"); fact.setExceptionCode("MANUAL_COMPLETE");
|
||||
fact.setQuantity("RETURN_BULK_SKU_LABEL".equals(operationType(item)) ? Math.toIntExact(missing) : 1);
|
||||
fact.setReason(request.remark()); fact.setOperatorKey(operatorKey); fact.setOperatorName(operatorName);
|
||||
fact.setOccurredAt(LocalDateTime.now()); unitFacts.insert(fact);
|
||||
task.setExceptionPackages(value(task.getExceptionPackages()) + 1);
|
||||
task.setExceptionPackages(value(task.getExceptionPackages()) + fact.getQuantity());
|
||||
}
|
||||
}
|
||||
List<Long> photoIds = request.photoFileIds().stream().distinct().toList();
|
||||
@@ -475,6 +560,7 @@ public class PdaRelabelService {
|
||||
resolvedNewLabel = trim(item.getFbaCartonMark());
|
||||
}
|
||||
fact.setOldLabel(normalizedOldLabel); fact.setNewLabel(resolvedNewLabel); fact.setResult(resultCode);
|
||||
fact.setQuantity(1);
|
||||
fact.setExceptionCode(exceptionCode); fact.setReason(trim(reason));
|
||||
fact.setOperatorKey(operatorKey); fact.setOperatorName(operatorName); fact.setOccurredAt(LocalDateTime.now());
|
||||
try { unitFacts.insert(fact); }
|
||||
@@ -622,15 +708,15 @@ public class PdaRelabelService {
|
||||
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"),
|
||||
value(item.getNotifiedPackageCount()), quantity(itemFacts, "RELABELLED"), quantity(itemFacts, "SKIPPED"),
|
||||
quantity(itemFacts, "EXCEPTION"),
|
||||
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "CARTON") ? 1 : 0),
|
||||
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "SKU") ? 1 : 0),
|
||||
item.getDestinationFbaWarehouseCode());
|
||||
}).toList();
|
||||
List<PdaRelabelResponses.UnitFact> factResponses = facts.stream().map(fact ->
|
||||
new PdaRelabelResponses.UnitFact(fact.getId(), fact.getItemId(), fact.getOldLabel(), fact.getNewLabel(),
|
||||
fact.getResult(), fact.getExceptionCode(), fact.getReason(), fact.getOperatorName(), fact.getOccurredAt())).toList();
|
||||
fact.getResult(), fact.getExceptionCode(), fact.getReason(), factQuantity(fact), fact.getOperatorName(), fact.getOccurredAt())).toList();
|
||||
List<PdaRelabelResponses.History> historyResponses = task == null ? List.of() : histories.selectList(
|
||||
new LambdaQueryWrapper<PdaRelabelHistory>().eq(PdaRelabelHistory::getCustomerId, application.getCustomerId())
|
||||
.eq(PdaRelabelHistory::getTaskId, task.getId())
|
||||
@@ -727,6 +813,9 @@ public class PdaRelabelService {
|
||||
|
||||
private String operationType(RelabelApplication application) {
|
||||
List<RelabelItem> items = itemList(application);
|
||||
if (!items.isEmpty() && items.stream().allMatch(item -> "RETURN_BULK_SKU_LABEL".equals(operationType(item)))) {
|
||||
return "RETURN_BULK_SKU_LABEL";
|
||||
}
|
||||
boolean sku = items.stream().anyMatch(item -> Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(operationType(item)));
|
||||
boolean carton = items.stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
return sku && carton ? "CARTON_AND_SKU" : sku ? "SKU_LABEL" : "CARTON_LABEL";
|
||||
@@ -748,6 +837,13 @@ public class PdaRelabelService {
|
||||
private static int count(List<PdaRelabelUnitFact> facts, String result) {
|
||||
return (int) facts.stream().filter(fact -> result.equals(fact.getResult())).count();
|
||||
}
|
||||
private static int quantity(List<PdaRelabelUnitFact> facts, String result) {
|
||||
return facts.stream().filter(fact -> result.equals(fact.getResult())).mapToInt(PdaRelabelService::factQuantity).sum();
|
||||
}
|
||||
private static int processedQuantity(List<PdaRelabelUnitFact> facts, Long itemId) {
|
||||
return facts.stream().filter(fact -> itemId.equals(fact.getItemId())).mapToInt(PdaRelabelService::factQuantity).sum();
|
||||
}
|
||||
private static int factQuantity(PdaRelabelUnitFact fact) { return fact.getQuantity() == null ? 1 : fact.getQuantity(); }
|
||||
private static int value(Integer value) { return value == null ? 0 : value; }
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String idempotencyKey(String value) { return value == null ? null : value.trim(); }
|
||||
|
||||
+72
-13
@@ -31,8 +31,10 @@ import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPdaTask;
|
||||
@@ -70,10 +72,11 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class RelabelApplicationService implements WalletResultHandler {
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
public static final String PENDING_PROCESSING = "PENDING_PROCESSING";
|
||||
public static final String CUSTOMER_CONFIRM_PENDING = "CUSTOMER_CONFIRM_PENDING";
|
||||
public static final String READY_FOR_OUTBOUND = "READY_FOR_OUTBOUND";
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_PROCESSING, "PROCESSING", CUSTOMER_CONFIRM_PENDING,
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, PENDING_PROCESSING, "PROCESSING", CUSTOMER_CONFIRM_PENDING,
|
||||
READY_FOR_OUTBOUND, "COMPLETED", "REJECTED", "CANCELLED");
|
||||
private static final Set<String> DELIVERY_METHODS = Set.of("TRUCK", "EXPRESS", "TRUCK_PICKUP", "EXPRESS_PICKUP", "PICKUP");
|
||||
|
||||
@@ -90,6 +93,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private ReturnOrderMapper returnOrders;
|
||||
@Resource private RelabelDocumentFileGateway documentFiles;
|
||||
@Resource private PdaRelabelTaskMapper pdaTasks;
|
||||
@Resource private RelabelPhotoFileGateway photoFiles;
|
||||
@@ -203,7 +207,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!StringUtils.hasText(customerId)) throw badRequest("客户不能为空");
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
if (!Set.of(PENDING_PROCESSING, "PROCESSING").contains(application.getStatus())) throw conflict("当前换标状态不允许修改资料");
|
||||
if (!Set.of(PENDING_REVIEW, PENDING_PROCESSING, "PROCESSING").contains(application.getStatus())) throw conflict("当前换标状态不允许修改资料");
|
||||
return application;
|
||||
}
|
||||
|
||||
@@ -270,9 +274,13 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (cartonSkus.isEmpty()) continue;
|
||||
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
|
||||
.collect(Collectors.joining(","));
|
||||
long availablePackages = value(carton.getTotalQuantity());
|
||||
if (availablePackages <= 0) {
|
||||
availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
|
||||
}
|
||||
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
|
||||
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
|
||||
carton.getDestinationWarehouseName(), skuCodes, "退货合箱库存", 1, 0, List.of(),
|
||||
carton.getDestinationWarehouseName(), skuCodes, "退货入库库存", availablePackages, 0, List.of(),
|
||||
"RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus));
|
||||
}
|
||||
return result;
|
||||
@@ -347,6 +355,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
int notified = 0;
|
||||
List<SourceInventory> sources = new java.util.ArrayList<>();
|
||||
Set<String> selectedCartonMarks = new HashSet<>();
|
||||
Map<Long, ReturnInventoryCarton> selectedReturnCartons = new LinkedHashMap<>();
|
||||
Map<Long, Long> selectedReturnPackages = new LinkedHashMap<>();
|
||||
for (RelabelRequests.Item item : request.items()) {
|
||||
validateOperation(item);
|
||||
if (item.notifiedPackageCount() > item.totalPackageCount()) {
|
||||
@@ -358,7 +368,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
throw badRequest("The same carton mark cannot create more than one relabel task");
|
||||
}
|
||||
if (item.returnCartonId() != null) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.returnCartonId());
|
||||
ReturnInventoryCarton carton = selectedReturnCartons.get(item.returnCartonId());
|
||||
if (carton == null) {
|
||||
carton = returnCartons.lockId(item.returnCartonId());
|
||||
if (carton != null) selectedReturnCartons.put(item.returnCartonId(), carton);
|
||||
}
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !request.operationWarehouseId().trim().equals(carton.getOverseasWarehouseId())) {
|
||||
throw conflict("所选退货箱不属于当前客户和操作仓");
|
||||
@@ -366,8 +380,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
|
||||
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
|
||||
}
|
||||
if (!carton.getCartonMark().equalsIgnoreCase(item.cartonMark().trim())) {
|
||||
throw conflict("退货箱仓库唛头已变化,请重新查询");
|
||||
long requestedPackages = selectedReturnPackages.merge(carton.getId(),
|
||||
(long) item.notifiedPackageCount(), Math::addExact);
|
||||
if (requestedPackages > value(carton.getTotalQuantity())) {
|
||||
throw conflict("退货托盘 " + carton.getCartonMark() + " 可用箱数不足");
|
||||
}
|
||||
sources.add(new SourceInventory(item, null, carton));
|
||||
continue;
|
||||
@@ -438,9 +454,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setUpdatedAt(now);
|
||||
applications.insert(application);
|
||||
|
||||
for (SourceInventory source : sources) {
|
||||
if (source.returnCarton() == null) continue;
|
||||
ReturnInventoryCarton carton = source.returnCarton();
|
||||
for (ReturnInventoryCarton carton : selectedReturnCartons.values()) {
|
||||
carton.setRelabelApplicationId(applicationId); carton.setRelabelNo(relabelNo);
|
||||
carton.setRelabelStatus("PENDING"); carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now);
|
||||
returnCartons.updateById(carton);
|
||||
@@ -530,6 +544,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
|
||||
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
|
||||
validateTransition(application.getStatus(), next);
|
||||
if (PENDING_REVIEW.equals(application.getStatus()) && PENDING_PROCESSING.equals(next)
|
||||
&& itemList(application.getId()).stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(operationType(item)))) {
|
||||
boolean hasCartonFile = StringUtils.hasText(application.getCartonTotalFileId());
|
||||
boolean hasSkuFile = StringUtils.hasText(application.getSkuTotalFileId());
|
||||
if (hasCartonFile == hasSkuFile) throw conflict("退货贴标审核前必须且只能上传一个箱唛总文件或 SKU 总文件");
|
||||
}
|
||||
if ("PROCESSING".equals(next)
|
||||
&& !Set.of("FROZEN", "NOT_REQUIRED").contains(application.getWalletStatus())) {
|
||||
throw conflict("Relabel funds must be frozen before processing");
|
||||
@@ -541,6 +561,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
applications.updateById(application);
|
||||
List<LastlegOrder> linkedOrders = tailOrders(application);
|
||||
if (linkedOrders.isEmpty()) throw conflict("Linked tail-leg order is missing");
|
||||
if (PENDING_PROCESSING.equals(next)) {
|
||||
application.setFulfillmentStatus("WAIT_RELABEL");
|
||||
applications.updateById(application);
|
||||
}
|
||||
if (CUSTOMER_CONFIRM_PENDING.equals(next)) {
|
||||
application.setFulfillmentStatus("WAIT_CUSTOMER_CONFIRMATION");
|
||||
applications.updateById(application);
|
||||
@@ -586,6 +610,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
case "REJECTED", "CANCELLED" -> "CANCELLED";
|
||||
default -> mainStatus;
|
||||
};
|
||||
// Reviewing/processing a return relabel must not move the already
|
||||
// completed inbound order back to assignment or relabel waiting.
|
||||
if ("RETURN_RELABEL".equals(order.getOrderType())
|
||||
&& !Set.of("COMPLETED", "REJECTED", "CANCELLED").contains(next)) {
|
||||
orderStatus = "INBOUND_COMPLETED";
|
||||
}
|
||||
order.setCurrentStatus(orderStatus);
|
||||
order.setSubOrderStatus("RELABEL:" + next);
|
||||
order.setRelabelStatus(businessRelabelStatus(next));
|
||||
@@ -595,6 +625,15 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
insertOrderHistory(customerId, order.getId(), orderBefore, orderStatus, request.remark(), now);
|
||||
}
|
||||
}
|
||||
ReturnOrder returnOrder = returnOrders.selectOne(new LambdaQueryWrapper<ReturnOrder>()
|
||||
.eq(ReturnOrder::getRelabelApplicationId, application.getId())
|
||||
.eq(ReturnOrder::getCustomerId, customerId)
|
||||
.last("LIMIT 1"));
|
||||
if (returnOrder != null) {
|
||||
returnOrder.setRelabelStatus(next);
|
||||
returnOrder.setUpdatedAt(now);
|
||||
returnOrders.updateById(returnOrder);
|
||||
}
|
||||
insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now);
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
@@ -689,7 +728,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
boolean requiresOutbound = relabelItems.stream()
|
||||
.anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
.anyMatch(item -> Set.of("CARTON_LABEL", "CARTON_AND_SKU").contains(operationType(item)));
|
||||
reviewTask.setStatus("COMPLETED"); reviewTask.setReviewDecision("CONFIRMED");
|
||||
reviewTask.setReviewerKey(operatorId); reviewTask.setReviewerName("客户确认");
|
||||
reviewTask.setReviewRemark(request.remark()); reviewTask.setReviewedAt(LocalDateTime.now());
|
||||
@@ -721,6 +760,22 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
request.remark(), now);
|
||||
}
|
||||
}
|
||||
ReturnOrder returnOrder = returnOrders.selectOne(new LambdaQueryWrapper<ReturnOrder>()
|
||||
.eq(ReturnOrder::getRelabelApplicationId, application.getId())
|
||||
.eq(ReturnOrder::getCustomerId, customerId)
|
||||
.last("LIMIT 1"));
|
||||
if (returnOrder != null) {
|
||||
returnOrder.setRelabelStatus("COMPLETED");
|
||||
returnOrder.setUpdatedAt(now);
|
||||
returnOrders.updateById(returnOrder);
|
||||
}
|
||||
for (RelabelItem item : relabelItems) {
|
||||
if (item.getReturnCartonId() == null) continue;
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !Objects.equals(carton.getRelabelApplicationId(), application.getId())) continue;
|
||||
carton.setRelabelStatus("COMPLETED"); carton.setRelabelCompletedAt(now);
|
||||
carton.setStatus("AVAILABLE"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
}
|
||||
insertHistory(customerId, application.getId(), before, application.getStatus(), operatorId,
|
||||
request.remark(), now);
|
||||
// 仓储转运换标在客户确认后立即恢复为可排车,发车时再转为 OUTBOUNDED。
|
||||
@@ -739,7 +794,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
transfer.setRelabelCompletedAt(now);
|
||||
transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
application.setFulfillmentStatus(transferStatus);
|
||||
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus)
|
||||
? "WAIT_PRE_DISPATCH" : transferStatus);
|
||||
applications.updateById(application);
|
||||
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
|
||||
LastlegOrder transferOrder = orders.selectById(transfer.getLastlegOrderId());
|
||||
@@ -1057,7 +1113,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
orderHistories.insert(value);
|
||||
}
|
||||
private static void validateTransition(String from, String to) {
|
||||
boolean allowed = PENDING_PROCESSING.equals(from) && Set.of("PROCESSING", "REJECTED", "CANCELLED").contains(to)
|
||||
boolean allowed = PENDING_REVIEW.equals(from) && Set.of(PENDING_PROCESSING, "REJECTED", "CANCELLED").contains(to)
|
||||
|| PENDING_PROCESSING.equals(from) && Set.of("PROCESSING", "REJECTED", "CANCELLED").contains(to)
|
||||
|| "PROCESSING".equals(from) && Set.of(CUSTOMER_CONFIRM_PENDING, "REJECTED", "CANCELLED").contains(to)
|
||||
|| CUSTOMER_CONFIRM_PENDING.equals(from) && Set.of(READY_FOR_OUTBOUND, "REJECTED", "CANCELLED").contains(to)
|
||||
|| READY_FOR_OUTBOUND.equals(from) && Set.of("COMPLETED", "CANCELLED").contains(to);
|
||||
@@ -1065,7 +1122,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
|
||||
private static String businessRelabelStatus(String workflowStatus) {
|
||||
if ("PROCESSING".equals(workflowStatus)) return "PROCESSING";
|
||||
if (Set.of(PENDING_REVIEW, PENDING_PROCESSING, "PROCESSING", "REJECTED", "CANCELLED").contains(workflowStatus)) {
|
||||
return workflowStatus;
|
||||
}
|
||||
return Set.of(CUSTOMER_CONFIRM_PENDING, READY_FOR_OUTBOUND, "COMPLETED").contains(workflowStatus)
|
||||
? "COMPLETED" : PENDING_PROCESSING;
|
||||
}
|
||||
|
||||
+11
-1
@@ -7,6 +7,7 @@ import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -20,12 +21,14 @@ public class RelabelBindingGuard {
|
||||
@Resource private RelabelItemMapper items;
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
public void requireBound(RelabelApplication app) {
|
||||
List<RelabelItem> cartons = items.selectList(new QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId()))
|
||||
.stream().filter(item -> !"SKU_LABEL".equals(item.getOperationType())).toList();
|
||||
.stream().filter(item -> Set.of("CARTON_LABEL", "CARTON_AND_SKU", "RETURN_BULK_SKU_LABEL")
|
||||
.contains(item.getOperationType())).toList();
|
||||
if (cartons.isEmpty()) return;
|
||||
Map<String, Set<String>> bindings = new HashMap<>();
|
||||
for (PdaRelabelHistory history : histories.selectList(new QueryWrapper<PdaRelabelHistory>()
|
||||
@@ -41,6 +44,13 @@ public class RelabelBindingGuard {
|
||||
} catch (Exception e) { throw conflict("组托记录无法读取,请核对后重试"); }
|
||||
}
|
||||
for (RelabelItem item : cartons) {
|
||||
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
|
||||
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
|
||||
if (carton == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode())
|
||||
|| !"PROCESSING".equals(carton.getRelabelStatus()))
|
||||
throw conflict("退货箱 " + item.getCartonMark() + " 尚未绑定带目的仓的新托盘,不能完成换标");
|
||||
continue;
|
||||
}
|
||||
var position = item.getSourceInventoryPositionId() == null ? null
|
||||
: positions.findTenantPosition(app.getCustomerId(), item.getSourceInventoryPositionId());
|
||||
if (position == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(position.getPalletNo())
|
||||
|
||||
+68
@@ -6,17 +6,28 @@ 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 com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
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.*;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway;
|
||||
|
||||
@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;
|
||||
@Resource private PalletPhysicalGateway physicalPallets;
|
||||
@Resource private ReturnInventoryCartonMapper inventoryCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper inventoryCartonItems;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper returnOrders;
|
||||
@Resource private RelabelPhotoFileGateway photoFiles;
|
||||
@PostMapping("/start") public ApiResponse<PdaReturnReceiptResponses.Start> start(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PdaReturnReceiptRequests.Start request) {
|
||||
return ApiResponse.success(service.start(PdaWarehouseIdentity.require(user), request));
|
||||
@@ -30,4 +41,61 @@ public class PdaReturnReceiptController {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.confirm(identity, request));
|
||||
}
|
||||
@PostMapping("/{returnNo}/manual-complete") public ApiResponse<com.baiyue.lastleg.returnreceipt.dto.ReturnOrderResponses.Detail> manualComplete(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo, @Valid @RequestBody PdaReturnReceiptRequests.ManualComplete request) {
|
||||
return ApiResponse.success(service.manualComplete(PdaWarehouseIdentity.require(user), returnNo, request));
|
||||
}
|
||||
public record Pallet(String palletCode, long palletId, String status, String printableHtml) { }
|
||||
public record Photo(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
@PostMapping(value = "/{returnNo}/photos/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ApiResponse<Photo> uploadPhoto(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String returnNo,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
var order = returnOrders.findActiveNo(String.valueOf(identity.overseasWarehouseId()), returnNo);
|
||||
if (order == null) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "当前仓没有该待签收退货单");
|
||||
if (file == null || file.isEmpty()) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST, "请选择退货打托照片");
|
||||
if (file.getSize() > 10L * 1024 * 1024) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST, "照片不能超过 10MB");
|
||||
if (file.getContentType() == null || !file.getContentType().toLowerCase(java.util.Locale.ROOT).startsWith("image/")) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST, "只能上传图片");
|
||||
var uploaded = photoFiles.upload(order.getCustomerId(), order.getId(), user.userId(), file);
|
||||
return ApiResponse.success(new Photo(uploaded.fileId(), uploaded.fileName(), uploaded.contentType(), uploaded.sizeBytes()));
|
||||
}
|
||||
@GetMapping("/{returnNo}/photos/files/{fileId}/download")
|
||||
public org.springframework.http.ResponseEntity<byte[]> downloadPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String returnNo, @PathVariable long fileId) {
|
||||
var file = service.downloadPhoto(PdaWarehouseIdentity.require(user), returnNo, fileId);
|
||||
return org.springframework.http.ResponseEntity.ok().contentType(org.springframework.http.MediaType.parseMediaType(file.contentType()))
|
||||
.header("Cache-Control", "no-store").body(file.content());
|
||||
}
|
||||
@PostMapping("/pallets") public ApiResponse<Pallet> createPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PdaReturnReceiptRequests.Pallet request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
if (returnOrders.findActiveNo(String.valueOf(identity.overseasWarehouseId()), request.returnNo()) == null) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "当前仓没有该待签收退货单");
|
||||
}
|
||||
var pallet = physicalPallets.ensureRelabelPallet(identity.overseasWarehouseId(),
|
||||
"RETURN:" + request.returnNo() + ":" + request.skuCode() + ":" + System.nanoTime());
|
||||
String code = org.springframework.web.util.HtmlUtils.htmlEscape(pallet.palletCode());
|
||||
String returnNo = org.springframework.web.util.HtmlUtils.htmlEscape(request.returnNo());
|
||||
String sku = org.springframework.web.util.HtmlUtils.htmlEscape(request.skuCode());
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code + "</title><style>@page{size:100mm 70mm;margin:4mm}body{font-family:Arial,sans-serif;text-align:center;margin:0}h1{font-size:24px;margin:7mm 0 3mm}.code{font-size:17px;font-weight:700;word-break:break-all}.ref{font-size:11px;margin-top:4mm}</style></head><body><h1>退货托盘标签</h1><div class=\"code\">" + code + "</div><div class=\"ref\">退货单:" + returnNo + "<br>SKU:" + sku + "</div></body></html>";
|
||||
return ApiResponse.success(new Pallet(pallet.palletCode(), pallet.palletId(), pallet.status(), html));
|
||||
}
|
||||
@PostMapping("/pallets/check") public ApiResponse<PdaReturnReceiptResponses.PalletCheck> checkPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PdaReturnReceiptRequests.CheckPallet request) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
var order = returnOrders.findActiveNo(String.valueOf(identity.overseasWarehouseId()), request.returnNo().trim());
|
||||
if (order == null) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "当前仓没有该待签收退货单");
|
||||
var physical = physicalPallets.resolve(request.palletCode().trim());
|
||||
if (physical == null || physical.warehouseId() != identity.overseasWarehouseId()) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "当前仓没有该托盘");
|
||||
}
|
||||
var inventory = inventoryCartons.listPalletInventory(String.valueOf(identity.overseasWarehouseId()), physical.palletCode());
|
||||
if (inventory.isEmpty()) return ApiResponse.success(new PdaReturnReceiptResponses.PalletCheck(physical.palletCode(), true, java.util.List.of(), 0));
|
||||
if (inventory.stream().anyMatch(carton -> !order.getReturnNo().equals(carton.getSourceReturnNo()))) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "该托盘已绑定其他退货单,不能用于当前退货单");
|
||||
}
|
||||
java.util.LinkedHashSet<String> skuCodes = new java.util.LinkedHashSet<>(); long quantity = 0;
|
||||
for (var carton : inventory) for (var item : inventoryCartonItems.listCarton(carton.getId())) { skuCodes.add(item.getSkuCode()); quantity += item.getQuantity() == null ? 0 : item.getQuantity(); }
|
||||
return ApiResponse.success(new PdaReturnReceiptResponses.PalletCheck(physical.palletCode(), false, java.util.List.copyOf(skuCodes), quantity));
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.returnreceipt.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
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.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/return-orders")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')")
|
||||
public class PlatformReturnOrderController {
|
||||
@Resource private ReturnReceiptService service;
|
||||
@PostMapping("/{returnNo}/review")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> review(@RequestHeader("X-Customer-Id") String customerId,
|
||||
@PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Review request) {
|
||||
return ApiResponse.success(service.review(customerId, returnNo, request));
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -12,10 +12,13 @@ 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 Pallet(@NotBlank @Size(max = 128) String returnNo, @NotBlank @Size(max = 128) String skuCode) { }
|
||||
public record CheckPallet(@NotBlank @Size(max = 128) String returnNo, @NotBlank @Size(max = 128) String palletCode) { }
|
||||
public record ManualComplete(@NotBlank @Size(max = 500) String discrepancyReason) { }
|
||||
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,
|
||||
@NotBlank @Size(max = 128) String palletCode,
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
public record Item(@NotBlank @Size(max = 128) String skuCode,
|
||||
|
||||
+5
-2
@@ -5,13 +5,16 @@ import java.util.List;
|
||||
|
||||
public final class PdaReturnReceiptResponses {
|
||||
private PdaReturnReceiptResponses() { }
|
||||
public record Start(String returnNo, String status, List<Item> expectedItems) { }
|
||||
public record Start(String returnNo, String status, List<Item> expectedItems, List<CompletedPallet> completedPallets, List<Photo> photos) { }
|
||||
public record Photo(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
public record CompletedPallet(String palletCode, String skuCode, long quantity, String receiptBatchNo) { }
|
||||
public record PalletCheck(String palletCode, boolean empty, List<String> skuCodes, long quantity) { }
|
||||
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,
|
||||
public record Confirmed(String returnNo, String receiptNo, String receiptBatchNo, String palletCode, String orderStatus,
|
||||
boolean fullyMatched, long expectedQuantity, long receivedQuantity,
|
||||
LocalDateTime signedAt) { }
|
||||
}
|
||||
|
||||
+8
-3
@@ -28,8 +28,13 @@ public final class ReturnOrderRequests {
|
||||
String trackingNo, String skuCode, String status) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
public record RelabelCreate(@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> cartonMarks,
|
||||
public record RelabelCreate(@NotEmpty @Size(max = 500) List<@Valid RelabelItem> items,
|
||||
@NotBlank @Size(max = 16) String fileScope,
|
||||
@NotBlank @Size(max = 128) String destinationFbaWarehouseId,
|
||||
@NotBlank @Size(max = 128) String destinationFbaWarehouseCode,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 128) String contactName, @Size(max = 64) String contactPhone,
|
||||
@Size(max = 32) String deliveryMethod, @Size(max = 1000) String remark) {}
|
||||
@Size(max = 1000) String cartonRemark) {}
|
||||
public record RelabelItem(@NotBlank @Size(max = 128) String skuCode,
|
||||
@Positive long quantity) {}
|
||||
public record Review(@NotBlank String decision, @Size(max = 500) String remark) { }
|
||||
}
|
||||
|
||||
+7
-4
@@ -8,14 +8,17 @@ public final class ReturnOrderResponses {
|
||||
public record Summary(String returnNo, String lastlegOrderNo, String overseasWarehouseId,
|
||||
String warehouseContactName, String warehouseContactPhone, String customerReferenceNo, String status,
|
||||
int skuTypes, long expectedQuantity, long receivedQuantity, boolean needRelabel,
|
||||
String relabelNo, String relabelStatus, LocalDateTime createdAt) { }
|
||||
String relabelNo, String relabelStatus, String discrepancyReason, 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,
|
||||
public record Detail(Summary order, String remark, List<Item> items, List<Receipt> receipts, List<PalletDetail> pallets, List<Photo> photos) { }
|
||||
public record Photo(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
public record PalletDetail(String palletCode, int skuTypes, long totalQuantity, List<PalletItem> items) { }
|
||||
public record PalletItem(String skuCode, String productNameZh, long quantity) { }
|
||||
public record Receipt(String receiptNo, String receiptBatchNo, String palletCode, String trackingNo, String startedBy,
|
||||
long receivedQuantity, String operatorName, LocalDateTime signedAt) { }
|
||||
public record RelabelCandidate(Long cartonId, String cartonMark, String sourceReturnNo, String status,
|
||||
long totalQuantity, List<Item> items, String relabelStatus) { }
|
||||
public record RelabelCreated(String relabelNo, Long relabelApplicationId, String returnNo,
|
||||
List<String> cartonMarks, String status) { }
|
||||
List<String> skuCodes, String status) { }
|
||||
}
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ public class ReturnInventoryCarton {
|
||||
private Long sourceReceiptId;
|
||||
private String sourceBatchNo;
|
||||
private String sourceReturnNo;
|
||||
private String palletCode;
|
||||
private String cartonMark;
|
||||
private String fbaCartonMark;
|
||||
private String destinationWarehouseCode;
|
||||
|
||||
@@ -28,6 +28,8 @@ public class ReturnOrder {
|
||||
private Long expectedQuantity;
|
||||
private Long receivedQuantity;
|
||||
private String remark;
|
||||
private String discrepancyReason;
|
||||
private LocalDateTime manuallyCompletedAt;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String createdBy;
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ public class ReturnReceipt {
|
||||
private String customerId;
|
||||
private String receiptNo;
|
||||
private String receiptBatchNo;
|
||||
private String palletCode;
|
||||
private String cartonMark;
|
||||
private String trackingNo;
|
||||
private String boundBy;
|
||||
|
||||
+17
@@ -5,6 +5,7 @@ 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 org.apache.ibatis.annotations.Update;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
@@ -35,9 +36,25 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE")
|
||||
ReturnInventoryCarton lockId(@Param("id") long id);
|
||||
|
||||
@Update("UPDATE ll_return_inventory_carton SET total_quantity=#{remaining},sku_types=1,status='AVAILABLE'," +
|
||||
"relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL,updated_at=NOW(3) " +
|
||||
"WHERE id=#{id} AND customer_id=#{customerId} AND relabel_application_id=#{applicationId} " +
|
||||
"AND status='RELABEL_PENDING' AND total_quantity>#{remaining}")
|
||||
int releasePartial(@Param("id") long id, @Param("customerId") String customerId,
|
||||
@Param("applicationId") long applicationId, @Param("remaining") long remaining);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} LIMIT 1")
|
||||
ReturnInventoryCarton findReceipt(@Param("receiptId") long receiptId);
|
||||
|
||||
@Select("SELECT c.* FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.customer_id=#{customerId} AND c.overseas_warehouse_id=#{warehouseId} AND c.pallet_code=#{palletCode} AND c.status='AVAILABLE' ORDER BY c.id LIMIT 1 FOR UPDATE")
|
||||
ReturnInventoryCarton findPalletSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE overseas_warehouse_id=#{warehouseId} AND pallet_code=#{palletCode} AND status='AVAILABLE' ORDER BY id LIMIT 1")
|
||||
ReturnInventoryCarton findPalletSkuForCheck(@Param("customerId") String ignoredCustomerId, @Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE overseas_warehouse_id=#{warehouseId} AND pallet_code=#{palletCode} AND status='AVAILABLE' ORDER BY id")
|
||||
List<ReturnInventoryCarton> listPalletInventory(@Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
|
||||
@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);
|
||||
|
||||
|
||||
+7
-1
@@ -3,6 +3,12 @@ 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;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnReceiptItemMapper extends BaseMapper<ReturnReceiptItem> { }
|
||||
public interface ReturnReceiptItemMapper extends BaseMapper<ReturnReceiptItem> {
|
||||
@Select("SELECT * FROM ll_return_receipt_item WHERE receipt_id=#{receiptId} ORDER BY id")
|
||||
List<ReturnReceiptItem> listReceipt(@Param("receiptId") long receiptId);
|
||||
}
|
||||
|
||||
+4
@@ -63,6 +63,9 @@ public class ReturnInventoryService {
|
||||
public ReturnInventoryCarton createReceiptCarton(ReturnOrder order, ReturnReceipt receipt,
|
||||
String requestedMark, List<ReceiptLine> lines) {
|
||||
if (lines.isEmpty()) throw new IllegalArgumentException("退货签收库存明细不能为空");
|
||||
if (lines.size() != 1) throw conflict("一个签收批次只能签收一个 SKU");
|
||||
String normalizedPalletCode = required(receipt.getPalletCode(), "托盘码不能为空");
|
||||
required(lines.get(0).skuCode(), "SKU 不能为空");
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
@@ -73,6 +76,7 @@ public class ReturnInventoryService {
|
||||
carton.setId(cartonId); carton.setCustomerId(order.getCustomerId()); carton.setOverseasWarehouseId(order.getOverseasWarehouseId());
|
||||
carton.setSourceReceiptId(receipt.getId()); carton.setSourceBatchNo(receipt.getReceiptBatchNo());
|
||||
carton.setSourceReturnNo(order.getReturnNo()); carton.setCartonMark(cartonMark);
|
||||
carton.setPalletCode(normalizedPalletCode);
|
||||
carton.setStatus(AVAILABLE); carton.setSkuTypes(lines.size()); carton.setTotalQuantity(total); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
for (ReceiptLine line : lines) {
|
||||
|
||||
+216
-35
@@ -25,6 +25,7 @@ import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
@@ -55,6 +56,7 @@ import java.util.Set;
|
||||
@Service
|
||||
public class ReturnReceiptService {
|
||||
public static final String PENDING_RECEIPT = "PENDING_RECEIPT";
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
|
||||
@@ -71,6 +73,8 @@ public class ReturnReceiptService {
|
||||
@Resource private ReturnInventoryCartonItemMapper cartonItems;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway photoFiles;
|
||||
@Resource private com.baiyue.lastleg.service.FbaWarehouseValidator fbaWarehouseValidator;
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail create(String customerId, String operatorId, ReturnOrderRequests.Create request) {
|
||||
@@ -92,7 +96,7 @@ public class ReturnReceiptService {
|
||||
order.setWarehouseContactName(text(request.warehouseContactName()));
|
||||
order.setWarehouseContactPhone(text(request.warehouseContactPhone()));
|
||||
order.setReturnNo(numbers.next("RETURN", order.getOverseasWarehouseId(), order.getId()));
|
||||
order.setCustomerReferenceNo(text(request.customerReferenceNo())); order.setNeedRelabel(request.needRelabel()); order.setStatus(PENDING_RECEIPT);
|
||||
order.setCustomerReferenceNo(text(request.customerReferenceNo())); order.setNeedRelabel(request.needRelabel()); order.setStatus(PENDING_REVIEW);
|
||||
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);
|
||||
@@ -121,8 +125,8 @@ public class ReturnReceiptService {
|
||||
tail.setSourceRegion("CUSTOMER"); tail.setSourceInboundId(returnOrder.getReturnNo());
|
||||
tail.setSourceOrderId(String.valueOf(returnOrder.getId()));
|
||||
tail.setSourceOrderNo(returnOrder.getReturnNo()); tail.setOverseasWarehouseId(returnOrder.getOverseasWarehouseId());
|
||||
tail.setCurrentStatus("PENDING_ASSIGNMENT"); tail.setSubOrderStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? "RETURN_RELABEL:PENDING_BINDING" : "RETURN_RELABEL:NOT_REQUIRED");
|
||||
tail.setRelabelStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? "PENDING_BINDING" : "NOT_REQUIRED");
|
||||
tail.setCurrentStatus(PENDING_REVIEW); tail.setSubOrderStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? "RETURN_RELABEL:PENDING_REVIEW" : "RETURN_RELABEL:NOT_REQUIRED");
|
||||
tail.setRelabelStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? PENDING_REVIEW : "NOT_REQUIRED");
|
||||
tail.setCustomerRemark(returnOrder.getRemark()); tail.setSourceEventId("RETURN_ORDER:" + returnOrder.getReturnNo());
|
||||
tail.setSourceLoadingPlanId("RETURN_RELABEL"); tail.setCreatedAt(now); tail.setUpdatedAt(now);
|
||||
tail.setSourcePayloadHash(returnOrder.getRequestHash());
|
||||
@@ -132,6 +136,30 @@ public class ReturnReceiptService {
|
||||
orders.updateById(returnOrder);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail updatePending(String customerId, String returnNo, ReturnOrderRequests.Create request) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货预报不存在");
|
||||
if (!PENDING_REVIEW.equals(order.getStatus())) throw conflict("只有待审核退货预报可以修改");
|
||||
Set<String> skus = new LinkedHashSet<>(); long expected = 0;
|
||||
for (ReturnOrderRequests.Item input : request.items()) { if (!skus.add(norm(input.skuCode()))) throw badRequest("同一退货单不能重复预报相同 SKU"); expected = Math.addExact(expected, input.expectedQuantity()); }
|
||||
order.setOverseasWarehouseId(text(request.overseasWarehouseId())); order.setWarehouseContactName(text(request.warehouseContactName()));
|
||||
order.setWarehouseContactPhone(text(request.warehouseContactPhone())); order.setCustomerReferenceNo(text(request.customerReferenceNo()));
|
||||
order.setNeedRelabel(request.needRelabel()); order.setRemark(text(request.remark())); order.setExpectedSkuTypes(skus.size()); order.setExpectedQuantity(expected); order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order);
|
||||
orderItems.delete(new LambdaQueryWrapper<ReturnOrderItem>().eq(ReturnOrderItem::getReturnOrderId, order.getId()));
|
||||
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(LocalDateTime.now()); item.setUpdatedAt(LocalDateTime.now()); orderItems.insert(item); }
|
||||
return detail(customerId, returnNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail cancelPending(String customerId, String returnNo) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货预报不存在");
|
||||
if (!PENDING_REVIEW.equals(order.getStatus())) throw conflict("只有待审核退货预报可以取消");
|
||||
order.setStatus("CANCELLED"); order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order);
|
||||
return detail(customerId, returnNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnOrderResponses.Summary> page(String customerId, ReturnOrderRequests.Query query) {
|
||||
LambdaQueryWrapper<ReturnOrder> w = new LambdaQueryWrapper<ReturnOrder>()
|
||||
@@ -166,11 +194,56 @@ public class ReturnReceiptService {
|
||||
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(),
|
||||
List<ReturnOrderItem> orderLines = orderItems.listOrder(order.getId());
|
||||
List<ReturnOrderResponses.Item> items = orderLines.stream().map(this::item).toList();
|
||||
List<ReturnReceipt> orderReceipts = receipts.listOrder(order.getId());
|
||||
List<ReturnOrderResponses.Receipt> receiptViews = orderReceipts.stream().map(receipt ->
|
||||
new ReturnOrderResponses.Receipt(receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getPalletCode(), receipt.getTrackingNo(),
|
||||
receipt.getBoundBy(), value(receipt.getReceivedQuantity()), receipt.getOperatorName(), receipt.getSignedAt())).toList();
|
||||
return new ReturnOrderResponses.Detail(summary(order), order.getRemark(), items, receiptViews);
|
||||
Map<String, Map<String, ReturnOrderResponses.PalletItem>> palletItems = new LinkedHashMap<>();
|
||||
Map<String, String> productNames = new LinkedHashMap<>();
|
||||
for (ReturnOrderItem line : orderLines) productNames.put(line.getSkuCode(), line.getProductNameZh());
|
||||
for (ReturnReceipt receipt : orderReceipts) {
|
||||
if (text(receipt.getPalletCode()) == null) continue;
|
||||
Map<String, ReturnOrderResponses.PalletItem> bySku = palletItems.computeIfAbsent(receipt.getPalletCode(), ignored -> new LinkedHashMap<>());
|
||||
for (ReturnReceiptItem line : receiptItems.listReceipt(receipt.getId())) bySku.merge(line.getSkuCode(),
|
||||
new ReturnOrderResponses.PalletItem(line.getSkuCode(), productNames.get(line.getSkuCode()), value(line.getReceivedQuantity())),
|
||||
(left, right) -> new ReturnOrderResponses.PalletItem(left.skuCode(), text(left.productNameZh()) == null ? right.productNameZh() : left.productNameZh(), left.quantity() + right.quantity()));
|
||||
}
|
||||
List<ReturnOrderResponses.PalletDetail> palletViews = palletItems.entrySet().stream().map(entry -> {
|
||||
List<ReturnOrderResponses.PalletItem> palletLines = List.copyOf(entry.getValue().values());
|
||||
return new ReturnOrderResponses.PalletDetail(entry.getKey(), palletLines.size(), palletLines.stream().mapToLong(ReturnOrderResponses.PalletItem::quantity).sum(), palletLines);
|
||||
}).toList();
|
||||
List<ReturnOrderResponses.Photo> photos = photoList(order).stream()
|
||||
.map(file -> new ReturnOrderResponses.Photo(file.fileId(), file.fileName(), file.contentType(), file.sizeBytes())).toList();
|
||||
return new ReturnOrderResponses.Detail(summary(order), order.getRemark(), items, receiptViews, palletViews, photos);
|
||||
}
|
||||
|
||||
public com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway.DownloadedFile downloadPhoto(String customerId, String returnNo, long fileId) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货单不存在");
|
||||
return photoFiles.download(order.getCustomerId(), order.getId(), fileId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail review(String customerId, String returnNo, ReturnOrderRequests.Review request) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货预报不存在");
|
||||
if (!PENDING_REVIEW.equals(order.getStatus())) throw conflict("只有待审核退货预报可以审核");
|
||||
String decision = norm(request.decision());
|
||||
if (!Set.of("APPROVE", "REJECT").contains(decision)) throw badRequest("decision 必须为 APPROVE 或 REJECT");
|
||||
if ("REJECT".equals(decision) && text(request.remark()) == null) throw badRequest("审核驳回必须填写原因");
|
||||
order.setStatus("APPROVE".equals(decision) ? PENDING_RECEIPT : "REJECTED");
|
||||
if ("REJECT".equals(decision)) order.setRemark(text(request.remark()));
|
||||
order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order);
|
||||
LastlegOrder tail = order.getLastlegOrderId() == null ? null : lastlegOrders.selectById(order.getLastlegOrderId());
|
||||
if (tail != null) {
|
||||
tail.setCurrentStatus("APPROVE".equals(decision) ? PENDING_RECEIPT : "REJECTED");
|
||||
tail.setSubOrderStatus("RETURN_RELABEL:" + ("APPROVE".equals(decision) ? PENDING_RECEIPT : "REJECTED"));
|
||||
tail.setRelabelStatus("APPROVE".equals(decision) && Boolean.TRUE.equals(order.getNeedRelabel()) ? "WAIT_INBOUND" : ("APPROVE".equals(decision) ? "NOT_REQUIRED" : "REJECTED"));
|
||||
tail.setUpdatedAt(LocalDateTime.now()); lastlegOrders.updateById(tail);
|
||||
}
|
||||
return detail(customerId, returnNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -195,44 +268,119 @@ public class ReturnReceiptService {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
if (order == null) throw notFound("退货单不存在");
|
||||
if (!COMPLETED.equals(order.getStatus())) throw conflict("退货单尚未完成签收");
|
||||
String fileScope = norm(request.fileScope());
|
||||
if (!Set.of("CARTON_TOTAL", "SKU_TOTAL").contains(fileScope)) throw badRequest("贴标总文件类型必须为 CARTON_TOTAL 或 SKU_TOTAL");
|
||||
String key = text(request.idempotencyKey());
|
||||
String requestHash = hash(request);
|
||||
RelabelApplication replay = relabelApplications.findKey(customerId, key);
|
||||
if (replay != null) return new ReturnOrderResponses.RelabelCreated(replay.getRelabelNo(), replay.getId(), order.getReturnNo(), request.cartonMarks(), replay.getStatus());
|
||||
List<ReturnInventoryCarton> selected = new ArrayList<>();
|
||||
if (replay != null) return new ReturnOrderResponses.RelabelCreated(replay.getRelabelNo(), replay.getId(),
|
||||
order.getReturnNo(), request.items().stream().map(ReturnOrderRequests.RelabelItem::skuCode).toList(), replay.getStatus());
|
||||
if (text(order.getRelabelNo()) != null) throw conflict("当前退货单已经生成贴标申请");
|
||||
var destination = fbaWarehouseValidator.required(request.destinationFbaWarehouseId(), request.destinationFbaWarehouseCode(), null);
|
||||
Map<String, ReturnOrderItem> receivedBySku = orderItems.lockOrder(order.getId()).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(item -> norm(item.getSkuCode()), item -> item));
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
for (String mark : request.cartonMarks()) {
|
||||
String normalized = text(mark);
|
||||
if (!seen.add(normalized.toUpperCase(java.util.Locale.ROOT))) throw badRequest("箱唛不能重复");
|
||||
ReturnInventoryCarton carton = cartons.findCustomerMark(customerId, normalized);
|
||||
if (carton == null || !order.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())) throw notFound("退货箱不存在: " + normalized);
|
||||
if (!ReturnInventoryService.AVAILABLE.equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) throw conflict("退货箱不可申请换标: " + normalized);
|
||||
if (carton.getSourceReturnNo() == null || !java.util.Arrays.asList(carton.getSourceReturnNo().split(",")).contains(order.getReturnNo())) throw conflict("退货箱不属于当前退货单: " + normalized);
|
||||
selected.add(cartons.lockMark(normalized));
|
||||
long totalQuantity = 0;
|
||||
for (ReturnOrderRequests.RelabelItem requested : request.items()) {
|
||||
String sku = norm(requested.skuCode());
|
||||
if (!seen.add(sku)) throw badRequest("同一贴标申请不能重复选择 SKU: " + requested.skuCode());
|
||||
ReturnOrderItem received = receivedBySku.get(sku);
|
||||
if (received == null || requested.quantity() > value(received.getReceivedQuantity())) {
|
||||
throw conflict("SKU " + requested.skuCode() + " 的贴标数量超过当前退货单实收数量");
|
||||
}
|
||||
totalQuantity = Math.addExact(totalQuantity, requested.quantity());
|
||||
}
|
||||
long appId = IdWorker.getId(); LocalDateTime now = LocalDateTime.now();
|
||||
String relabelNo = numbers.next("RELABEL", order.getOverseasWarehouseId(), appId);
|
||||
LastlegOrder tail = new LastlegOrder(); tail.setId(IdWorker.getId()); tail.setOrderNo(numbers.next("RELABEL", order.getOverseasWarehouseId(), tail.getId()));
|
||||
tail.setCustomerId(customerId); tail.setOrderType("RELABEL_APPLICATION"); tail.setSourceRegion("CUSTOMER"); tail.setSourceOrderId(String.valueOf(appId)); tail.setSourceOrderNo(relabelNo);
|
||||
tail.setOverseasWarehouseId(order.getOverseasWarehouseId()); tail.setCurrentStatus("PENDING_ASSIGNMENT"); tail.setSubOrderStatus("RELABEL:RETURN_CARTON"); tail.setRelabelStatus("RETURN_CARTON"); tail.setPackageCount(selected.size());
|
||||
tail.setProductQuantity(Math.toIntExact(selected.stream().mapToLong(c -> value(c.getTotalQuantity())).sum())); tail.setCreatedAt(now); tail.setUpdatedAt(now); lastlegOrders.insert(tail);
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(appId); app.setCustomerId(customerId); app.setRelabelNo(relabelNo); app.setLastlegOrderId(tail.getId());
|
||||
app.setCustomerOrderNo(order.getReturnNo()); app.setOverseasWarehouseId(order.getOverseasWarehouseId()); app.setContactName(text(request.contactName())); app.setContactPhone(text(request.contactPhone())); app.setDeliveryMethod(text(request.deliveryMethod()) == null ? "TRUCK" : text(request.deliveryMethod()));
|
||||
app.setFulfillmentStatus("WAIT_RELABEL"); app.setRemark(text(request.remark())); app.setStatus("PENDING_PROCESSING"); app.setItemCount(selected.size()); app.setTotalPackageCount(selected.size()); app.setNotifiedPackageCount(selected.size()); app.setChargeAmount(java.math.BigDecimal.ZERO); app.setChargeCurrency("CNY"); app.setWalletStatus("NOT_REQUIRED"); app.setIdempotencyKey(key); app.setRequestHash(hash(request)); app.setCreatedBy(text(operatorId)); app.setCreatedAt(now); app.setUpdatedAt(now); relabelApplications.insert(app);
|
||||
for (ReturnInventoryCarton carton : selected) {
|
||||
for (ReturnInventoryCartonItem ci : cartonItems.listCarton(carton.getId())) {
|
||||
RelabelItem item = new RelabelItem(); item.setId(IdWorker.getId()); item.setApplicationId(appId); item.setCustomerId(customerId); item.setSkuCode(ci.getSkuCode()); item.setProductNameZh(ci.getProductNameZh()); item.setTotalPackageCount(1); item.setNotifiedPackageCount(1); item.setShipmentNo(order.getReturnNo()); item.setCartonMark(carton.getCartonMark()); item.setFbaCartonMark(carton.getFbaCartonMark()); item.setOperationType("CARTON_LABEL"); item.setReturnCartonId(carton.getId()); item.setSourceCartonMark(carton.getCartonMark()); item.setCreatedAt(now); relabelItems.insert(item);
|
||||
}
|
||||
carton.setRelabelApplicationId(appId); carton.setRelabelNo(relabelNo); carton.setRelabelStatus("PENDING"); carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now); cartons.updateById(carton);
|
||||
LastlegOrder tail = order.getLastlegOrderId() == null ? null : lastlegOrders.selectById(order.getLastlegOrderId());
|
||||
if (tail == null) {
|
||||
tail = new LastlegOrder(); tail.setId(IdWorker.getId()); tail.setOrderNo(numbers.next("RELABEL", order.getOverseasWarehouseId(), tail.getId()));
|
||||
tail.setCustomerId(customerId); tail.setOrderType("RELABEL_APPLICATION"); tail.setSourceRegion("CUSTOMER");
|
||||
tail.setSourceInboundId("RETURN_RELABEL:" + relabelNo);
|
||||
tail.setSourceEventId("RETURN_RELABEL:" + relabelNo);
|
||||
tail.setSourceLoadingPlanId("RETURN_RELABEL");
|
||||
tail.setSourcePayloadHash("sha256:" + requestHash);
|
||||
tail.setSourceOrderId(String.valueOf(appId)); tail.setSourceOrderNo(relabelNo);
|
||||
tail.setOverseasWarehouseId(order.getOverseasWarehouseId()); tail.setCurrentStatus("WAIT_RELABEL");
|
||||
tail.setCreatedAt(now); tail.setPackageCount(Math.toIntExact(totalQuantity));
|
||||
tail.setProductQuantity(Math.toIntExact(totalQuantity)); tail.setUpdatedAt(now); lastlegOrders.insert(tail);
|
||||
}
|
||||
order.setRelabelNo(relabelNo); order.setRelabelStatus("PENDING_PROCESSING"); orders.updateById(order);
|
||||
return new ReturnOrderResponses.RelabelCreated(relabelNo, appId, order.getReturnNo(), selected.stream().map(ReturnInventoryCarton::getCartonMark).toList(), app.getStatus());
|
||||
// A return relabel application belongs to the already received return tail-leg order.
|
||||
// Do not create a second independent tail order or overwrite the inbound status.
|
||||
tail.setRelabelOrderNo(relabelNo);
|
||||
tail.setRelabelStatus("PENDING_REVIEW");
|
||||
tail.setSubOrderStatus("RETURN_RELABEL:PENDING_REVIEW");
|
||||
tail.setCurrentStatus("RETURN_RELABEL".equals(tail.getOrderType()) ? "INBOUND_COMPLETED" : tail.getCurrentStatus());
|
||||
tail.setPackageCount(Math.toIntExact(totalQuantity));
|
||||
tail.setProductQuantity(Math.toIntExact(totalQuantity));
|
||||
tail.setUpdatedAt(now);
|
||||
lastlegOrders.updateById(tail);
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(appId); app.setCustomerId(customerId); app.setRelabelNo(relabelNo); app.setLastlegOrderId(tail.getId());
|
||||
app.setCustomerOrderNo(order.getReturnNo()); app.setOverseasWarehouseId(order.getOverseasWarehouseId());
|
||||
app.setLabelDeadline(now.plusDays(7));
|
||||
app.setContactName(text(order.getWarehouseContactName())); app.setContactPhone(text(order.getWarehouseContactPhone()));
|
||||
app.setFulfillmentStatus("WAIT_APPROVAL"); app.setRemark(text(request.cartonRemark())); app.setStatus("PENDING_REVIEW"); app.setItemCount(request.items().size()); app.setTotalPackageCount(Math.toIntExact(totalQuantity)); app.setNotifiedPackageCount(Math.toIntExact(totalQuantity)); app.setChargeAmount(java.math.BigDecimal.ZERO); app.setChargeCurrency("CNY"); app.setWalletStatus("NOT_REQUIRED"); app.setIdempotencyKey(key); app.setRequestHash(requestHash); app.setCreatedBy(text(operatorId)); app.setCreatedAt(now); app.setUpdatedAt(now); relabelApplications.insert(app);
|
||||
List<ReturnInventoryCarton> availableCartons = cartons.findAvailable(customerId, order.getOverseasWarehouseId());
|
||||
Map<Long, ReturnInventoryCarton> selectedCartons = new LinkedHashMap<>();
|
||||
for (ReturnOrderRequests.RelabelItem requested : request.items()) {
|
||||
ReturnOrderItem source = receivedBySku.get(norm(requested.skuCode()));
|
||||
long remaining = requested.quantity();
|
||||
for (ReturnInventoryCarton candidate : availableCartons) {
|
||||
if (remaining == 0) break;
|
||||
if (!order.getReturnNo().equals(candidate.getSourceReturnNo())) continue;
|
||||
long skuQuantity = cartonItems.listCarton(candidate.getId()).stream()
|
||||
.filter(line -> norm(requested.skuCode()).equals(norm(line.getSkuCode())))
|
||||
.mapToLong(line -> value(line.getQuantity())).sum();
|
||||
if (skuQuantity <= 0) continue;
|
||||
ReturnInventoryCarton carton = selectedCartons.computeIfAbsent(candidate.getId(), ignored -> cartons.lockId(candidate.getId()));
|
||||
if (carton == null || !"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null)
|
||||
throw conflict("退货箱已被其他换标申请占用,请刷新后重试");
|
||||
long allocated = Math.min(remaining, skuQuantity);
|
||||
RelabelItem item = new RelabelItem(); item.setId(IdWorker.getId()); item.setApplicationId(appId); item.setCustomerId(customerId);
|
||||
item.setSkuCode(source.getSkuCode());
|
||||
item.setProductNameZh(text(source.getProductNameZh()) == null ? source.getSkuCode() : text(source.getProductNameZh()));
|
||||
item.setTotalPackageCount(Math.toIntExact(allocated)); item.setNotifiedPackageCount(Math.toIntExact(allocated));
|
||||
item.setShipmentNo(order.getReturnNo()); item.setLabelRequirement(text(request.cartonRemark()));
|
||||
item.setDestinationWarehouseId(String.valueOf(destination.id())); item.setDestinationFbaWarehouseCode(destination.code());
|
||||
item.setOperationType("RETURN_BULK_SKU_LABEL"); item.setReturnCartonId(carton.getId());
|
||||
item.setCartonMark(carton.getCartonMark()); item.setSourceCartonMark(carton.getCartonMark());
|
||||
item.setCreatedAt(now); relabelItems.insert(item);
|
||||
remaining -= allocated;
|
||||
}
|
||||
if (remaining > 0) throw conflict("退货 SKU 可用实收数量不足: " + requested.skuCode());
|
||||
}
|
||||
for (ReturnInventoryCarton carton : selectedCartons.values()) {
|
||||
carton.setRelabelApplicationId(appId); carton.setRelabelNo(relabelNo); carton.setRelabelStatus("PENDING");
|
||||
carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now); cartons.updateById(carton);
|
||||
}
|
||||
order.setRelabelApplicationId(appId); order.setRelabelNo(relabelNo); order.setRelabelStatus("PENDING_REVIEW"); orders.updateById(order);
|
||||
return new ReturnOrderResponses.RelabelCreated(relabelNo, appId, order.getReturnNo(),
|
||||
request.items().stream().map(ReturnOrderRequests.RelabelItem::skuCode).toList(), app.getStatus());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PdaReturnReceiptResponses.Start start(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.Start request) {
|
||||
ReturnOrder order = activeOrder(String.valueOf(identity.overseasWarehouseId()), request.returnNo());
|
||||
List<PdaReturnReceiptResponses.CompletedPallet> completedPallets = receipts.listOrder(order.getId()).stream()
|
||||
.filter(receipt -> text(receipt.getPalletCode()) != null)
|
||||
.collect(java.util.stream.Collectors.toMap(ReturnReceipt::getPalletCode, receipt -> {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
ReturnInventoryCartonItem item = carton == null ? null : cartonItems.listCarton(carton.getId()).stream().findFirst().orElse(null);
|
||||
return new PdaReturnReceiptResponses.CompletedPallet(receipt.getPalletCode(), item == null ? "" : item.getSkuCode(),
|
||||
item == null ? 0 : value(item.getQuantity()), receipt.getReceiptBatchNo());
|
||||
}, (left, right) -> new PdaReturnReceiptResponses.CompletedPallet(left.palletCode(), left.skuCode(),
|
||||
left.quantity() + right.quantity(), right.receiptBatchNo()), java.util.LinkedHashMap::new))
|
||||
.values().stream().toList();
|
||||
List<PdaReturnReceiptResponses.Photo> photos = photoList(order).stream()
|
||||
.map(file -> new PdaReturnReceiptResponses.Photo(file.fileId(), file.fileName(), file.contentType(), file.sizeBytes())).toList();
|
||||
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());
|
||||
.filter(item -> value(item.getReceivedQuantity()) < value(item.getExpectedQuantity())).map(this::pdaItem).toList(), completedPallets, photos);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway.DownloadedFile downloadPhoto(PdaWarehouseIdentity identity, String returnNo, long fileId) {
|
||||
ReturnOrder order = activeOrder(String.valueOf(identity.overseasWarehouseId()), returnNo);
|
||||
return photoFiles.download(order.getCustomerId(), order.getId(), fileId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -255,6 +403,16 @@ public class ReturnReceiptService {
|
||||
return new PdaReturnReceiptResponses.SkuCandidates(skuCode, candidates);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail manualComplete(PdaWarehouseIdentity identity, String returnNo, PdaReturnReceiptRequests.ManualComplete request) {
|
||||
ReturnOrder order = activeOrder(String.valueOf(identity.overseasWarehouseId()), returnNo);
|
||||
if (value(order.getReceivedQuantity()) >= value(order.getExpectedQuantity())) throw conflict("退货单已按预期数量签收完成,不需要手动完结");
|
||||
order.setStatus("COMPLETED_WITH_DISCREPANCY"); order.setDiscrepancyReason(text(request.discrepancyReason()));
|
||||
order.setManuallyCompletedAt(LocalDateTime.now()); order.setUpdatedAt(LocalDateTime.now()); orders.updateById(order);
|
||||
syncReturnTail(order, "INBOUND_COMPLETED", true);
|
||||
return detail(order.getCustomerId(), order.getReturnNo());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaReturnReceiptResponses.Confirmed confirm(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.Confirm request) {
|
||||
String key = text(request.idempotencyKey());
|
||||
@@ -289,6 +447,7 @@ public class ReturnReceiptService {
|
||||
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.setPalletCode(text(request.palletCode()));
|
||||
receipt.setReceivedQuantity(receiptQuantity); receipt.setOperatorId(identity.operatorKey());
|
||||
receipt.setOperatorName(identity.operatorName()); receipt.setIdempotencyKey(key); receipt.setRequestHash(requestHash);
|
||||
receipt.setSignedAt(now); receipt.setCreatedAt(now);
|
||||
@@ -315,13 +474,20 @@ public class ReturnReceiptService {
|
||||
left.skuCode(), left.productNameZh() == null ? right.productNameZh() : left.productNameZh(),
|
||||
Math.addExact(left.quantity(), right.quantity())));
|
||||
}
|
||||
receipt.setCartonMark(inventory.createReceiptCarton(order, receipt, request.cartonMark(),
|
||||
if (allocations.stream().map(allocation -> allocation.item().getSkuCode()).distinct().count() != 1) {
|
||||
throw conflict("一个签收批次只能签收一个 SKU");
|
||||
}
|
||||
receipt.setCartonMark(inventory.createReceiptCarton(order, receipt, text(request.palletCode()),
|
||||
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()));
|
||||
if (completed && photoFiles != null && photoList(order).isEmpty()) {
|
||||
throw conflict("本次将完成签收,请先拍摄并上传退货打托照片");
|
||||
}
|
||||
order.setReceivedQuantity(received); order.setStatus(completed ? COMPLETED : PARTIALLY_RECEIVED); order.setUpdatedAt(now); orders.updateById(order);
|
||||
syncReturnTail(order, completed ? "INBOUND_COMPLETED" : "PARTIALLY_RECEIVED", completed || received > 0);
|
||||
ReturnReceiptBatch batch = new ReturnReceiptBatch();
|
||||
batch.setId(IdWorker.getId()); batch.setReturnOrderId(order.getId()); batch.setReceiptId(receiptId);
|
||||
batch.setCustomerId(order.getCustomerId()); batch.setBatchNo(receipt.getReceiptBatchNo());
|
||||
@@ -330,6 +496,21 @@ public class ReturnReceiptService {
|
||||
return confirmed(order, receipt, completed);
|
||||
}
|
||||
|
||||
private void syncReturnTail(ReturnOrder order, String currentStatus, boolean inboundStarted) {
|
||||
if (order.getLastlegOrderId() == null) return;
|
||||
LastlegOrder tail = lastlegOrders.selectById(order.getLastlegOrderId());
|
||||
if (tail == null) return;
|
||||
tail.setCurrentStatus(currentStatus);
|
||||
tail.setSubOrderStatus("RETURN_RELABEL:" + currentStatus);
|
||||
tail.setRelabelStatus(Boolean.TRUE.equals(order.getNeedRelabel()) && inboundStarted ? "PENDING_PROCESSING" : "NOT_REQUIRED");
|
||||
tail.setUpdatedAt(LocalDateTime.now());
|
||||
lastlegOrders.updateById(tail);
|
||||
}
|
||||
|
||||
private List<RelabelPhotoFileGateway.UploadedFile> photoList(ReturnOrder order) {
|
||||
return photoFiles == null ? List.of() : photoFiles.list(order.getCustomerId(), order.getId());
|
||||
}
|
||||
|
||||
private ReturnOrder activeOrder(String warehouseId, String returnNo) {
|
||||
ReturnOrder order = orders.findActiveNo(warehouseId, text(returnNo));
|
||||
if (order == null) throw notFound("当前仓没有待签收或签收中的退货单");
|
||||
@@ -367,13 +548,13 @@ public class ReturnReceiptService {
|
||||
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(),
|
||||
return new PdaReturnReceiptResponses.Confirmed(order.getReturnNo(), receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getPalletCode(),
|
||||
order.getStatus(), completed, value(order.getExpectedQuantity()), value(order.getReceivedQuantity()), receipt.getSignedAt());
|
||||
}
|
||||
private ReturnOrderResponses.Summary summary(ReturnOrder order) { return new ReturnOrderResponses.Summary(order.getReturnNo(), order.getLastlegOrderNo(),
|
||||
order.getOverseasWarehouseId(), order.getWarehouseContactName(), order.getWarehouseContactPhone(), order.getCustomerReferenceNo(), order.getStatus(),
|
||||
order.getExpectedSkuTypes() == null ? 0 : order.getExpectedSkuTypes(), value(order.getExpectedQuantity()),
|
||||
value(order.getReceivedQuantity()), Boolean.TRUE.equals(order.getNeedRelabel()), order.getRelabelNo(), order.getRelabelStatus(), order.getCreatedAt()); }
|
||||
value(order.getReceivedQuantity()), Boolean.TRUE.equals(order.getNeedRelabel()), order.getRelabelNo(), order.getRelabelStatus(), order.getDiscrepancyReason(), 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(),
|
||||
|
||||
@@ -33,7 +33,7 @@ public class FbaWarehouseValidator {
|
||||
|| !"FBA".equals(warehouse.warehouseType())
|
||||
|| !"ACTIVE".equals(warehouse.status())
|
||||
|| !same(requestedCode, warehouse.warehouseCode())
|
||||
|| !same(requestedName, warehouse.warehouseName())) {
|
||||
|| !sameIfProvided(requestedName, warehouse.warehouseName())) {
|
||||
throw invalid();
|
||||
}
|
||||
String region = firstRegion(response.data().addresses());
|
||||
@@ -69,6 +69,10 @@ public class FbaWarehouseValidator {
|
||||
&& requested.trim().equals(actual.trim());
|
||||
}
|
||||
|
||||
private boolean sameIfProvided(String requested, String actual) {
|
||||
return !StringUtils.hasText(requested) || same(requested, actual);
|
||||
}
|
||||
|
||||
private String firstRegion(List<DomesticWarehouseDirectoryClient.Address> addresses) {
|
||||
if (addresses == null) return null;
|
||||
return addresses.stream().filter(Objects::nonNull)
|
||||
|
||||
+9
-1
@@ -478,6 +478,14 @@ public class WarehouseTransferService {
|
||||
orderDetails.updateById(detail);
|
||||
}
|
||||
syncInboundPlanDestination(customerId, transfer);
|
||||
if (StringUtils.hasText(transfer.getRelabelNo())) {
|
||||
RelabelApplication relabel = relabelApplications.lockNo(customerId, transfer.getRelabelNo());
|
||||
if (relabel != null && "WAIT_FBA_DESTINATION".equals(relabel.getFulfillmentStatus())) {
|
||||
relabel.setFulfillmentStatus("WAIT_PRE_DISPATCH");
|
||||
relabel.setUpdatedAt(now);
|
||||
relabelApplications.updateById(relabel);
|
||||
}
|
||||
}
|
||||
insertOrderHistory(customerId, order.getId(), order.getCurrentStatus(), order.getCurrentStatus(), transferNo,
|
||||
"Warehouse transfer destination bound by " + trim(operatorId), now);
|
||||
results.add(new WarehouseTransferResponses.DestinationResult(customerId, transferNo));
|
||||
@@ -1160,7 +1168,7 @@ public class WarehouseTransferService {
|
||||
throw conflict("Source inventory balance is missing for warehouse transfer receipt");
|
||||
}
|
||||
InventoryCommandResponse result = inventory.stockIn(customerId, operatorId,
|
||||
new InventoryCommandRequests.StockIn(customerId, null,
|
||||
new InventoryCommandRequests.StockIn(customerId, sourceBalance.getCustomerName(),
|
||||
request.destinationOverseasWarehouseId().trim(), trim(request.destinationOverseasWarehouseCode()),
|
||||
trim(request.destinationOverseasWarehouseName()), trim(request.destinationWarehouseId()),
|
||||
request.destinationWarehouseCode().trim(), trim(request.destinationWarehouseName()),
|
||||
|
||||
Reference in New Issue
Block a user