贴标申请,仓储转运

This commit is contained in:
2026-10-13 13:46:16 +08:00
parent cebcdfb66e
commit 89a05a2348
11 changed files with 75 additions and 31 deletions
@@ -2,7 +2,7 @@
-- MySQL 8.0+;只允许目标测试环境的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。 -- MySQL 8.0+;只允许目标测试环境的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。
-- 字段、索引、约束及注释直接定义在 CREATE TABLE 内;不回填、不清理、不改造存量。 -- 字段、索引、约束及注释直接定义在 CREATE TABLE 内;不回填、不清理、不改造存量。
-- 国内 IAM 管理两区域权限;海外不访问国内库。执行方法及系统种子清单见同目录 README.md。 -- 国内 IAM 管理两区域权限;海外不访问国内库。执行方法及系统种子清单见同目录 README.md。
-- DDL 隐式提交;失败后停止,不从中间续跑,也不把部分初始化当成成功。 -- DDL 隐式提交;失败后停止,不从中间续跑,也不把部分初始化当成成功。utf8mb4_0900_ai_ci
SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci; SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci;
SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
@@ -4,4 +4,9 @@ import java.time.LocalDateTime;
/** A relabel application keeps its own status within the received parent order. */ /** A relabel application keeps its own status within the received parent order. */
public record RelabelSubOrder(Long lastlegOrderId, String relabelNo, String status, public record RelabelSubOrder(Long lastlegOrderId, String relabelNo, String status,
Integer quantity, LocalDateTime createdAt) { } Integer quantity, String quantityUnit, LocalDateTime createdAt) {
public RelabelSubOrder(Long lastlegOrderId, String relabelNo, String status,
Integer quantity, LocalDateTime createdAt) {
this(lastlegOrderId, relabelNo, status, quantity, "件", createdAt);
}
}
@@ -56,7 +56,8 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
"WHERE g.container_booking_id=ll_order.source_container_booking_id " + "WHERE g.container_booking_id=ll_order.source_container_booking_id " +
"AND g.node_sequence>=40 AND g.actual_at IS NOT NULL) THEN 1 ELSE 0 END END)={0})"; "AND g.node_sequence>=40 AND g.actual_at IS NOT NULL) THEN 1 ELSE 0 END END)={0})";
@Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity,r.created_at " + @Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity, " +
"CASE WHEN NOT EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=r.id AND ri.operation_type&lt;&gt;'CARTON_LABEL') THEN '箱' ELSE '件' END AS quantity_unit,r.created_at " +
"FROM ll_relabel_application r JOIN ll_order o ON o.id=r.lastleg_order_id AND o.customer_id=r.customer_id " + "FROM ll_relabel_application r JOIN ll_order o ON o.id=r.lastleg_order_id AND o.customer_id=r.customer_id " +
"WHERE r.lastleg_order_id IN <foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " + "WHERE r.lastleg_order_id IN <foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"ORDER BY r.created_at,r.id</script>") "ORDER BY r.created_at,r.id</script>")
@@ -34,7 +34,7 @@ public final class PalletOperationRequests {
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets, @NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
@Size(max = 500) String reason) { } @Size(max = 500) String reason) { }
/** actualSource为现场确认的原入仓库存行;cartonMarks确认本批目标绑定,不推断历史箱唛位置。 */ /** source为空时表示退货箱没有来源托盘,现场仅扫描箱唛后绑定目标托盘;actualSource为现场确认的原入仓库存行。 */
public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey, public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey,
@NotBlank @Size(max = 64) String relabelNo, @NotBlank @Size(max = 64) String relabelNo,
@Valid PalletExpectation source, @Valid PalletExpectation source,
@@ -105,13 +105,16 @@ public class PdaPalletOperationService {
|| !Objects.equals(carton.getOverseasWarehouseId(), app.getOverseasWarehouseId()) || !Objects.equals(carton.getOverseasWarehouseId(), app.getOverseasWarehouseId())
|| !Objects.equals(carton.getStatus(), "RELABEL_PENDING")) || !Objects.equals(carton.getStatus(), "RELABEL_PENDING"))
throw conflict("退货换标来源托盘不存在或已被占用"); throw conflict("退货换标来源托盘不存在或已被占用");
PalletPhysicalGateway.PalletReference pallet = physical.resolve(carton.getPalletCode()); String palletCode = text(carton.getPalletCode());
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId()) PalletPhysicalGateway.PalletReference pallet = palletCode.isBlank() || "UNBOUND".equalsIgnoreCase(palletCode)
? null : physical.resolve(palletCode);
if (palletCode.isBlank() || "UNBOUND".equalsIgnoreCase(palletCode)) palletCode = "UNBOUND";
if (pallet != null && pallet.warehouseId() != identity.overseasWarehouseId())
throw conflict("退货换标来源托盘不属于当前仓库"); throw conflict("退货换标来源托盘不属于当前仓库");
long quantity = entry.getValue().stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) long quantity = entry.getValue().stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(item.getOperationType()))
? value(carton.getTotalQuantity()) : 1; ? value(carton.getTotalQuantity()) : 1;
result.add(new PalletOperationResponses.RelabelSource(carton.getId(), 0L, result.add(new PalletOperationResponses.RelabelSource(carton.getId(), 0L,
pallet.palletId(), pallet.palletCode(), null, carton.getSourceBatchNo(), quantity, quantity, pallet == null ? null : pallet.palletId(), palletCode, null, carton.getSourceBatchNo(), quantity, quantity,
entry.getValue().stream().map(RelabelItem::getCartonMark).filter(Objects::nonNull).toList())); entry.getValue().stream().map(RelabelItem::getCartonMark).filter(Objects::nonNull).toList()));
} }
return result; return result;
@@ -225,11 +228,15 @@ public class PdaPalletOperationService {
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) { if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
throw conflict("换标单不属于 Token 冻结的海外仓"); throw conflict("换标单不属于 Token 冻结的海外仓");
} }
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(), if (request.target() == null) throw badRequest("目标托盘不能为空");
request.source(), List.of(request.target())); PalletPhysicalGateway.PalletReference target = resolvePallets(identity.overseasWarehouseId(),
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode())); List.of(request.target())).get(code(request.target().palletCode()));
PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode())); PalletPhysicalGateway.PalletReference source = request.source() == null ? null
if (source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同"); : resolvePallets(identity.overseasWarehouseId(), List.of(request.source())).get(code(request.source().palletCode()));
if (source != null && source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同");
Map<String, PalletPhysicalGateway.PalletReference> pallets = new LinkedHashMap<>();
if (source != null) pallets.put(source.palletCode(), source);
pallets.put(target.palletCode(), target);
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(value -> !value.isEmpty()).toList(); List<String> marks = request.cartonMarks().stream().map(String::trim).filter(value -> !value.isEmpty()).toList();
if (marks.size() != request.cartonMarks().size() || new HashSet<>(marks).size() != marks.size()) { if (marks.size() != request.cartonMarks().size() || new HashSet<>(marks).size() != marks.size()) {
throw badRequest("箱唛不能为空且不能重复"); throw badRequest("箱唛不能为空且不能重复");
@@ -245,6 +252,13 @@ public class PdaPalletOperationService {
Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>(); Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>();
Map<Long, InventoryPosition> anchors = new LinkedHashMap<>(); Map<Long, InventoryPosition> anchors = new LinkedHashMap<>();
boolean returnRelabel = selected.stream().allMatch(item -> item.getReturnCartonId() != null); boolean returnRelabel = selected.stream().allMatch(item -> item.getReturnCartonId() != null);
boolean unboundReturnRelabel = returnRelabel && selected.stream().allMatch(item -> {
ReturnInventoryCarton carton = returnCartons.selectById(item.getReturnCartonId());
return carton != null && (!org.springframework.util.StringUtils.hasText(carton.getPalletCode())
|| "UNBOUND".equalsIgnoreCase(carton.getPalletCode()));
});
if (unboundReturnRelabel && source != null) throw conflict("无来源托盘的退货箱不能提交来源托盘");
if (!unboundReturnRelabel && source == null) throw conflict("请先确认来源托盘");
InventoryPosition actual = returnRelabel || request.actualSource() == null InventoryPosition actual = returnRelabel || request.actualSource() == null
? null : positions.selectById(request.actualSource().positionId()); ? null : positions.selectById(request.actualSource().positionId());
if (!returnRelabel && request.actualSource() != null && (actual == null if (!returnRelabel && request.actualSource() != null && (actual == null
@@ -269,7 +283,7 @@ public class PdaPalletOperationService {
} }
} }
if (returnRelabel) { if (returnRelabel) {
return bindReturnRelabelPallet(identity, application, selected, source, target, request); return bindReturnRelabelPallet(identity, application, selected, source, target, request, unboundReturnRelabel);
} }
for (RelabelItem item : selected) { for (RelabelItem item : selected) {
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
@@ -313,7 +327,7 @@ public class PdaPalletOperationService {
private PalletOperationResponses.Operation bindReturnRelabelPallet(PdaWarehouseIdentity identity, private PalletOperationResponses.Operation bindReturnRelabelPallet(PdaWarehouseIdentity identity,
RelabelApplication application, List<RelabelItem> selected, RelabelApplication application, List<RelabelItem> selected,
PalletPhysicalGateway.PalletReference source, PalletPhysicalGateway.PalletReference target, PalletPhysicalGateway.PalletReference source, PalletPhysicalGateway.PalletReference target,
PalletOperationRequests.RelabelDisassemble request) { PalletOperationRequests.RelabelDisassemble request, boolean unboundSource) {
requireRelabelDestination(identity.overseasWarehouseId(), selected, target); requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
Set<String> alreadyBound = boundRelabelMarks(application); Set<String> alreadyBound = boundRelabelMarks(application);
boolean rebind = request.actualSource() == null && selected.stream().allMatch(item -> alreadyBound.contains(item.getCartonMark())); boolean rebind = request.actualSource() == null && selected.stream().allMatch(item -> alreadyBound.contains(item.getCartonMark()));
@@ -324,14 +338,15 @@ public class PdaPalletOperationService {
for (RelabelItem item : selected) { for (RelabelItem item : selected) {
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId()); ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId()) if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId())
|| !Objects.equals(carton.getPalletCode(), source.palletCode()) || (unboundSource ? (org.springframework.util.StringUtils.hasText(carton.getPalletCode()) && !"UNBOUND".equalsIgnoreCase(carton.getPalletCode()))
: !Objects.equals(carton.getPalletCode(), source.palletCode()))
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId()) || !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|| !Objects.equals(carton.getStatus(), "RELABEL_PENDING")) || !Objects.equals(carton.getStatus(), "RELABEL_PENDING"))
throw conflict("退货换标来源托盘或库存状态已变化,请刷新后重试"); throw conflict("退货换标来源托盘或库存状态已变化,请刷新后重试");
quantity += value(item.getNotifiedPackageCount()); quantity += value(item.getNotifiedPackageCount());
cartons.add(carton); cartons.add(carton);
} }
if (!rebind && (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId() if (!rebind && !unboundSource && (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|| request.actualSource().packages() != quantity)) || request.actualSource().packages() != quantity))
throw conflict("退货换标拆出数量必须等于本批已完成贴标数量"); throw conflict("退货换标拆出数量必须等于本批已完成贴标数量");
String key = required(request.idempotencyKey(), "幂等键不能为空"); String key = required(request.idempotencyKey(), "幂等键不能为空");
@@ -345,7 +360,8 @@ public class PdaPalletOperationService {
RelabelItem item = selected.get(index); RelabelItem item = selected.get(index);
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId()); ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId()) if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId())
|| !Objects.equals(carton.getPalletCode(), source.palletCode()) || (unboundSource ? (org.springframework.util.StringUtils.hasText(carton.getPalletCode()) && !"UNBOUND".equalsIgnoreCase(carton.getPalletCode()))
: !Objects.equals(carton.getPalletCode(), source.palletCode()))
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId()) || !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|| !"RELABEL_PENDING".equals(carton.getStatus())) || !"RELABEL_PENDING".equals(carton.getStatus()))
throw conflict("退货换标来源库存已变化,请刷新后重试"); throw conflict("退货换标来源库存已变化,请刷新后重试");
@@ -363,9 +379,10 @@ public class PdaPalletOperationService {
} }
} }
recordReturnRelabelBinding(identity, application, selected, target.palletCode()); recordReturnRelabelBinding(identity, application, selected, target.palletCode());
List<PalletOperationResponses.PalletContent> contents = List.of( List<PalletOperationResponses.PalletContent> contents = source == null
content(identity.overseasWarehouseId(), physical.resolve(source.palletCode())), ? List.of(content(identity.overseasWarehouseId(), physical.resolve(target.palletCode())))
content(identity.overseasWarehouseId(), physical.resolve(target.palletCode()))); : List.of(content(identity.overseasWarehouseId(), physical.resolve(source.palletCode())),
content(identity.overseasWarehouseId(), physical.resolve(target.palletCode())));
PalletOperationResponses.Operation result = new PalletOperationResponses.Operation(operation.getId(), PalletOperationResponses.Operation result = new PalletOperationResponses.Operation(operation.getId(),
operation.getOperationType(), key, "COMPLETED", false, LocalDateTime.now(), contents); operation.getOperationType(), key, "COMPLETED", false, LocalDateTime.now(), contents);
operation.setStatus("COMPLETED"); operation.setResultJson(write(result)); operation.setStatus("COMPLETED"); operation.setResultJson(write(result));
@@ -19,7 +19,8 @@ public final class RelabelResponses {
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl, String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt, String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
String reviewChannel, String reviewRemark, LocalDateTime createdAt, String reviewChannel, String reviewRemark, LocalDateTime createdAt,
LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt, LocalDateTime outboundAt) {} LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt, LocalDateTime outboundAt,
String productNameZh, String labelRequirement) {}
public record Milestones(LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt, public record Milestones(LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt,
LocalDateTime outboundAt) {} LocalDateTime outboundAt) {}
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName, public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
@@ -1187,7 +1187,11 @@ public class RelabelApplicationService implements WalletResultHandler {
private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) { private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) {
LastlegOrder order = orders.selectById(value.getLastlegOrderId()); LastlegOrder order = orders.selectById(value.getLastlegOrderId());
RelabelResponses.Milestones milestones = applications.findMilestones(value.getCustomerId(), value.getId()); RelabelResponses.Milestones milestones = applications.findMilestones(value.getCustomerId(), value.getId());
String skuCodes = detailItems.stream().map(RelabelItem::getSkuCode).filter(Objects::nonNull).distinct().collect(Collectors.joining(",")); String skuCodes = detailItems.stream().flatMap(item -> {
List<RelabelResponses.SkuItem> skuItems = readSkuItems(item.getSkuRelabelItemsJson());
return skuItems.isEmpty() ? java.util.Arrays.stream(Objects.requireNonNullElse(item.getSkuCode(), "").split(","))
: skuItems.stream().map(RelabelResponses.SkuItem::skuCode);
}).filter(StringUtils::hasText).map(String::trim).distinct().collect(Collectors.joining(","));
return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(), return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(),
order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(), order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(),
value.getOverseasWarehouseId(), value.getOverseasWarehouseId(),
@@ -1206,7 +1210,11 @@ public class RelabelApplicationService implements WalletResultHandler {
null, null,
value.getCreatedAt(), milestones == null ? null : milestones.reviewedAt(), value.getCreatedAt(), milestones == null ? null : milestones.reviewedAt(),
milestones == null ? null : milestones.relabelCompletedAt(), milestones == null ? null : milestones.relabelCompletedAt(),
milestones == null ? null : milestones.outboundAt()); milestones == null ? null : milestones.outboundAt(),
detailItems.stream().map(RelabelItem::getProductNameZh).filter(StringUtils::hasText)
.map(String::trim).distinct().collect(Collectors.joining("、")),
detailItems.stream().map(RelabelItem::getLabelRequirement).filter(StringUtils::hasText)
.map(String::trim).distinct().collect(Collectors.joining(";")));
} }
@Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); } @Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); }
@@ -27,13 +27,13 @@ public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventory
@Param("warehouseId") String warehouseId, @Param("quantity") long quantity); @Param("warehouseId") String warehouseId, @Param("quantity") long quantity);
String FREE_STOCK_COLUMNS = "b.id,b.customer_id,b.overseas_warehouse_id,b.sku_code,b.product_name_zh,b.total_quantity,b.carton_quantity,b.created_at,b.updated_at," + String FREE_STOCK_COLUMNS = "b.id,b.customer_id,b.overseas_warehouse_id,b.sku_code,b.product_name_zh,b.total_quantity,b.carton_quantity,b.created_at,b.updated_at," +
"LEAST(b.available_quantity,COALESCE((SELECT SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))) " + "LEAST(b.available_quantity,COALESCE((SELECT SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))) " +
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " + "FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id COLLATE utf8mb4_0900_ai_ci=c.customer_id COLLATE utf8mb4_0900_ai_ci " +
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id AND i.sku_code=b.sku_code " + "WHERE c.customer_id COLLATE utf8mb4_0900_ai_ci=b.customer_id COLLATE utf8mb4_0900_ai_ci AND c.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci=b.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci AND i.sku_code COLLATE utf8mb4_0900_ai_ci=b.sku_code COLLATE utf8mb4_0900_ai_ci " +
"AND c.status='AVAILABLE' AND c.count_status='CONFIRMED'),0)) AS available_quantity," + "AND c.status='AVAILABLE' AND c.count_status='CONFIRMED'),0)) AS available_quantity," +
"COALESCE((SELECT SUM(i.quantity) FROM ll_return_inventory_carton c " + "COALESCE((SELECT SUM(i.quantity) FROM ll_return_inventory_carton c " +
"JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " + "JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id COLLATE utf8mb4_0900_ai_ci=c.customer_id COLLATE utf8mb4_0900_ai_ci " +
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id " + "WHERE c.customer_id COLLATE utf8mb4_0900_ai_ci=b.customer_id COLLATE utf8mb4_0900_ai_ci AND c.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci=b.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci " +
"AND i.sku_code=b.sku_code AND c.status='RELABEL_PENDING'),0) AS relabel_locked_quantity"; "AND i.sku_code COLLATE utf8mb4_0900_ai_ci=b.sku_code COLLATE utf8mb4_0900_ai_ci AND c.status='RELABEL_PENDING'),0) AS relabel_locked_quantity";
@Select("SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " + @Select("SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
"AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} FOR UPDATE") "AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} FOR UPDATE")
ReturnInventoryBalance lockSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, ReturnInventoryBalance lockSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
@@ -72,8 +72,8 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
"OR ((SELECT c.relabel_application_id FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) IS NULL " + "OR ((SELECT c.relabel_application_id FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) IS NULL " +
"AND NOT EXISTS(SELECT 1 FROM ll_relabel_item newer JOIN ll_relabel_application na ON na.id=newer.application_id " + "AND NOT EXISTS(SELECT 1 FROM ll_relabel_item newer JOIN ll_relabel_application na ON na.id=newer.application_id " +
"WHERE newer.return_carton_id=ri.return_carton_id AND na.status NOT IN ('CANCELLED','REJECTED') " + "WHERE newer.return_carton_id=ri.return_carton_id AND na.status NOT IN ('CANCELLED','REJECTED') " +
"AND (COALESCE(newer.source_carton_mark,newer.carton_mark)=COALESCE(ri.source_carton_mark,ri.carton_mark) " + "AND (COALESCE(newer.source_carton_mark,newer.carton_mark) COLLATE utf8mb4_0900_ai_ci=COALESCE(ri.source_carton_mark,ri.carton_mark) COLLATE utf8mb4_0900_ai_ci " +
"OR COALESCE(newer.source_carton_mark,newer.carton_mark)=(SELECT c.carton_mark FROM ll_return_inventory_carton c WHERE c.id=#{cartonId})) " + "OR COALESCE(newer.source_carton_mark,newer.carton_mark) COLLATE utf8mb4_0900_ai_ci=(SELECT c.carton_mark FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) COLLATE utf8mb4_0900_ai_ci) " +
"AND (na.created_at>ra.created_at OR (na.created_at=ra.created_at AND na.id>ra.id)))))"; "AND (na.created_at>ra.created_at OR (na.created_at=ra.created_at AND na.id>ra.id)))))";
String ALL_COMPLETION_DEDUCTION = "COALESCE((SELECT SUM(j.quantity) FROM ll_relabel_pda_history h " + String ALL_COMPLETION_DEDUCTION = "COALESCE((SELECT SUM(j.quantity) FROM ll_relabel_pda_history h " +
"JOIN JSON_TABLE(CASE WHEN h.action='RETURN_COMPLETION_TRANSFER' AND JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END,'$.items[*]' COLUMNS(sku_code VARCHAR(128) PATH '$.skuCode',quantity BIGINT PATH '$.quantity')) j " + "JOIN JSON_TABLE(CASE WHEN h.action='RETURN_COMPLETION_TRANSFER' AND JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END,'$.items[*]' COLUMNS(sku_code VARCHAR(128) PATH '$.skuCode',quantity BIGINT PATH '$.quantity')) j " +
@@ -3064,8 +3064,11 @@ components:
labelDeadline: { type: string, format: date-time } labelDeadline: { type: string, format: date-time }
status: { $ref: '#/components/schemas/RelabelStatus' } status: { $ref: '#/components/schemas/RelabelStatus' }
itemCount: { type: integer } itemCount: { type: integer }
skuCodes: { type: string, description: 汇总全部箱内 skuItems 的旧 SKU 并去重,以逗号分隔;无 SKU 换标子明细时汇总主明细 SKU }
totalPackageCount: { type: integer } totalPackageCount: { type: integer }
notifiedPackageCount: { type: integer } notifiedPackageCount: { type: integer }
productNameZh: { type: string, description: 明细非空中文品名按原顺序去重并以顿号连接 }
labelRequirement: { type: string, description: 明细非空贴标要求按原顺序去重并以中文分号连接 }
deliveryMethod: { type: string, nullable: true, enum: [TRUCK, PICKUP, EXPRESS] } deliveryMethod: { type: string, nullable: true, enum: [TRUCK, PICKUP, EXPRESS] }
printShippingLabel: { type: boolean } printShippingLabel: { type: boolean }
shippingLabelFileId: { type: string, nullable: true } shippingLabelFileId: { type: string, nullable: true }
@@ -74,11 +74,20 @@ class RelabelApplicationServiceTest {
var outbound = reviewed.plusHours(2); var outbound = reviewed.plusHours(2);
when(applications.findMilestones("10001", 42L)) when(applications.findMilestones("10001", 42L))
.thenReturn(new RelabelResponses.Milestones(reviewed, completed, outbound)); .thenReturn(new RelabelResponses.Milestones(reviewed, completed, outbound));
RelabelResponses.Summary result = ReflectionTestUtils.invokeMethod(service, "summary", application, List.of()); var first = new RelabelItem(); first.setProductNameZh(" 商品甲 "); first.setLabelRequirement(" 贴标要求111 ");
first.setSkuCode("SKU-FIRST");
first.setSkuRelabelItemsJson("[{\"skuCode\":\"SKU-FIRST\",\"newSkuCode\":\"NEW-FIRST\",\"quantity\":10},{\"skuCode\":\"SKU-SECOND\",\"newSkuCode\":\"NEW-SECOND\",\"quantity\":5}]");
var second = new RelabelItem(); second.setProductNameZh("商品乙"); second.setLabelRequirement("贴标要求111");
var empty = new RelabelItem(); empty.setProductNameZh(" ");
RelabelResponses.Summary result = ReflectionTestUtils.invokeMethod(service, "summary", application, List.of(first, second, empty));
assertNotNull(result); assertNotNull(result);
assertEquals(reviewed, result.reviewedAt()); assertEquals(reviewed, result.reviewedAt());
assertEquals(completed, result.relabelCompletedAt()); assertEquals(completed, result.relabelCompletedAt());
assertEquals(outbound, result.outboundAt()); assertEquals(outbound, result.outboundAt());
assertEquals("商品甲、商品乙", result.productNameZh());
assertEquals("贴标要求111", result.labelRequirement());
assertEquals("SKU-FIRST,SKU-SECOND", result.skuCodes());
assertEquals(3, result.itemCount());
} }
@Test void returnInventoryCandidateUsesRealProductNames() { @Test void returnInventoryCandidateUsesRealProductNames() {