From 89a05a23481294f8686cba029c8ac8ffd3cb75cb Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 13 Oct 2026 13:46:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=B4=E6=A0=87=E7=94=B3=E8=AF=B7=EF=BC=8C?= =?UTF-8?q?=E4=BB=93=E5=82=A8=E8=BD=AC=E8=BF=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 2 +- .../baiyue/lastleg/dto/RelabelSubOrder.java | 7 ++- .../lastleg/mapper/LastlegOrderMapper.java | 3 +- .../dto/PalletOperationRequests.java | 2 +- .../service/PdaPalletOperationService.java | 49 +++++++++++++------ .../lastleg/relabel/dto/RelabelResponses.java | 3 +- .../service/RelabelApplicationService.java | 12 ++++- .../mapper/ReturnInventoryBalanceMapper.java | 10 ++-- .../mapper/ReturnInventoryCartonMapper.java | 4 +- .../openapi/overseas-business-v1.yaml | 3 ++ .../RelabelApplicationServiceTest.java | 11 ++++- 11 files changed, 75 insertions(+), 31 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 5abf094fb..db5c6cbdd 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -2,7 +2,7 @@ -- MySQL 8.0+;只允许目标测试环境的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。 -- 字段、索引、约束及注释直接定义在 CREATE TABLE 内;不回填、不清理、不改造存量。 -- 国内 IAM 管理两区域权限;海外不访问国内库。执行方法及系统种子清单见同目录 README.md。 --- DDL 隐式提交;失败后停止,不从中间续跑,也不把部分初始化当成成功。 +-- DDL 隐式提交;失败后停止,不从中间续跑,也不把部分初始化当成成功。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'; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/RelabelSubOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/RelabelSubOrder.java index b9e4bac5e..15b22c027 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/RelabelSubOrder.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/RelabelSubOrder.java @@ -4,4 +4,9 @@ import java.time.LocalDateTime; /** A relabel application keeps its own status within the received parent order. */ 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); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java index 103c4d6c9..c0d87a9c8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java @@ -56,7 +56,8 @@ public interface LastlegOrderMapper extends BaseMapper { "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})"; - @Select("") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationRequests.java index 3194e89d6..fd0d1e492 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationRequests.java @@ -34,7 +34,7 @@ public final class PalletOperationRequests { @NotEmpty @Size(max = 20) List<@Valid TargetMove> targets, @Size(max = 500) String reason) { } - /** actualSource为现场确认的原入仓库存行;cartonMarks确认本批目标绑定,不推断历史箱唛位置。 */ + /** source为空时表示退货箱没有来源托盘,现场仅扫描箱唛后绑定目标托盘;actualSource为现场确认的原入仓库存行。 */ public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey, @NotBlank @Size(max = 64) String relabelNo, @Valid PalletExpectation source, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java index aa85c1e4b..ae804f1b5 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java @@ -105,13 +105,16 @@ public class PdaPalletOperationService { || !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()) + String palletCode = text(carton.getPalletCode()); + 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("退货换标来源托盘不属于当前仓库"); long quantity = entry.getValue().stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) ? value(carton.getTotalQuantity()) : 1; 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())); } return result; @@ -225,11 +228,15 @@ public class PdaPalletOperationService { if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) { throw conflict("换标单不属于 Token 冻结的海外仓"); } - Map pallets = resolvePallets(identity.overseasWarehouseId(), - request.source(), List.of(request.target())); - PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode())); - PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode())); - if (source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同"); + if (request.target() == null) throw badRequest("目标托盘不能为空"); + PalletPhysicalGateway.PalletReference target = resolvePallets(identity.overseasWarehouseId(), + List.of(request.target())).get(code(request.target().palletCode())); + PalletPhysicalGateway.PalletReference source = request.source() == null ? null + : resolvePallets(identity.overseasWarehouseId(), List.of(request.source())).get(code(request.source().palletCode())); + if (source != null && source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同"); + Map pallets = new LinkedHashMap<>(); + if (source != null) pallets.put(source.palletCode(), source); + pallets.put(target.palletCode(), target); List marks = request.cartonMarks().stream().map(String::trim).filter(value -> !value.isEmpty()).toList(); if (marks.size() != request.cartonMarks().size() || new HashSet<>(marks).size() != marks.size()) { throw badRequest("箱唛不能为空且不能重复"); @@ -245,6 +252,13 @@ public class PdaPalletOperationService { Map> itemsByPosition = new LinkedHashMap<>(); Map anchors = new LinkedHashMap<>(); 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 ? null : positions.selectById(request.actualSource().positionId()); if (!returnRelabel && request.actualSource() != null && (actual == null @@ -269,7 +283,7 @@ public class PdaPalletOperationService { } } if (returnRelabel) { - return bindReturnRelabelPallet(identity, application, selected, source, target, request); + return bindReturnRelabelPallet(identity, application, selected, source, target, request, unboundReturnRelabel); } for (RelabelItem item : selected) { if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null @@ -313,7 +327,7 @@ public class PdaPalletOperationService { private PalletOperationResponses.Operation bindReturnRelabelPallet(PdaWarehouseIdentity identity, RelabelApplication application, List selected, PalletPhysicalGateway.PalletReference source, PalletPhysicalGateway.PalletReference target, - PalletOperationRequests.RelabelDisassemble request) { + PalletOperationRequests.RelabelDisassemble request, boolean unboundSource) { requireRelabelDestination(identity.overseasWarehouseId(), selected, target); Set alreadyBound = boundRelabelMarks(application); boolean rebind = request.actualSource() == null && selected.stream().allMatch(item -> alreadyBound.contains(item.getCartonMark())); @@ -324,14 +338,15 @@ public class PdaPalletOperationService { 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()) + || (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.getStatus(), "RELABEL_PENDING")) throw conflict("退货换标来源托盘或库存状态已变化,请刷新后重试"); quantity += value(item.getNotifiedPackageCount()); 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)) throw conflict("退货换标拆出数量必须等于本批已完成贴标数量"); String key = required(request.idempotencyKey(), "幂等键不能为空"); @@ -345,7 +360,8 @@ public class PdaPalletOperationService { 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()) + || (unboundSource ? (org.springframework.util.StringUtils.hasText(carton.getPalletCode()) && !"UNBOUND".equalsIgnoreCase(carton.getPalletCode())) + : !Objects.equals(carton.getPalletCode(), source.palletCode())) || !Objects.equals(carton.getRelabelApplicationId(), application.getId()) || !"RELABEL_PENDING".equals(carton.getStatus())) throw conflict("退货换标来源库存已变化,请刷新后重试"); @@ -363,9 +379,10 @@ public class PdaPalletOperationService { } } recordReturnRelabelBinding(identity, application, selected, target.palletCode()); - List contents = List.of( - content(identity.overseasWarehouseId(), physical.resolve(source.palletCode())), - content(identity.overseasWarehouseId(), physical.resolve(target.palletCode()))); + List contents = source == null + ? List.of(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(), operation.getOperationType(), key, "COMPLETED", false, LocalDateTime.now(), contents); operation.setStatus("COMPLETED"); operation.setResultJson(write(result)); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java index 47391ee83..7854f98b1 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java @@ -19,7 +19,8 @@ public final class RelabelResponses { String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl, String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt, 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, LocalDateTime outboundAt) {} public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index 37e764e0c..6e72bba6d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -1187,7 +1187,11 @@ public class RelabelApplicationService implements WalletResultHandler { private RelabelResponses.Summary summary(RelabelApplication value, List detailItems) { LastlegOrder order = orders.selectById(value.getLastlegOrderId()); 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 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(), order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(), value.getOverseasWarehouseId(), @@ -1206,7 +1210,11 @@ public class RelabelApplicationService implements WalletResultHandler { null, value.getCreatedAt(), milestones == null ? null : milestones.reviewedAt(), 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); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java index 7f95a645e..50b0809f4 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java @@ -27,13 +27,13 @@ public interface ReturnInventoryBalanceMapper extends BaseMapperra.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 " + "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 " + diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 0a9e32e30..8d5571c86 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -3064,8 +3064,11 @@ components: labelDeadline: { type: string, format: date-time } status: { $ref: '#/components/schemas/RelabelStatus' } itemCount: { type: integer } + skuCodes: { type: string, description: 汇总全部箱内 skuItems 的旧 SKU 并去重,以逗号分隔;无 SKU 换标子明细时汇总主明细 SKU } totalPackageCount: { type: integer } notifiedPackageCount: { type: integer } + productNameZh: { type: string, description: 明细非空中文品名按原顺序去重并以顿号连接 } + labelRequirement: { type: string, description: 明细非空贴标要求按原顺序去重并以中文分号连接 } deliveryMethod: { type: string, nullable: true, enum: [TRUCK, PICKUP, EXPRESS] } printShippingLabel: { type: boolean } shippingLabelFileId: { type: string, nullable: true } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java index c0a365f33..20608ec9d 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java @@ -74,11 +74,20 @@ class RelabelApplicationServiceTest { var outbound = reviewed.plusHours(2); when(applications.findMilestones("10001", 42L)) .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); assertEquals(reviewed, result.reviewedAt()); assertEquals(completed, result.relabelCompletedAt()); 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() {