From c8a99f9fff16643216e714d631a508d2f6c242ed Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 18 Sep 2026 14:58:40 +0800 Subject: [PATCH] fix: complete order measurement downstream flow --- deploy/sql/海外区域统一增量-MySQL8.sql | 8 +++ firstleg/AGENTS.md | 2 +- .../firstleg/client/OrderCargoClient.java | 13 ++++ .../InternalInboundOrderController.java | 12 ++++ .../baiyue/firstleg/dto/OrderRemarksSync.java | 22 ++++++ .../mapper/LoadingPlanItemMapper.java | 67 ++++++++++++++++++- .../firstleg/mapper/LoadingPlanMapper.java | 14 +++- .../service/OrderDeclarationSyncService.java | 3 + .../service/OrderRemarksSyncService.java | 57 ++++++++++++++++ .../firstleg/service/SmartLoadingService.java | 23 ++++++- .../main/resources/openapi/firstleg-v1.yaml | 26 +++++++ ...alInboundOrderReferenceControllerTest.java | 3 + .../OrderDeclarationSyncServiceTest.java | 1 + .../service/OrderRemarksSyncServiceTest.java | 50 ++++++++++++++ .../service/SmartLoadingServiceTest.java | 8 +++ .../delivery/dto/DeliveryResponses.java | 8 ++- .../delivery/entity/OutboundOrder.java | 4 ++ .../delivery/entity/OutboundPalletItem.java | 4 ++ .../service/DeliveryTransferService.java | 42 +++++------- .../dispatch/dto/DispatchResponse.java | 1 + .../lastleg/dispatch/entity/DispatchItem.java | 2 + .../dispatch/service/DispatchService.java | 7 +- .../lastleg/dto/LastlegOrderSummary.java | 4 ++ .../dto/InboundPlanListResponse.java | 5 +- .../inboundplan/dto/InboundPlanResponse.java | 5 +- .../mapper/InboundPlanOrderMapper.java | 13 ++++ .../mapper/InboundPlanQueryMapper.java | 4 ++ .../service/InboundPlanService.java | 15 ++++- .../lastleg/mapper/LastlegOrderMapper.java | 12 ++++ .../lastleg/service/LastlegOrderService.java | 22 ++++-- .../src/main/resources/db/lastleg.sql | 9 ++- .../openapi/overseas-business-v1.yaml | 18 +++-- .../service/DeliveryTransferServiceTest.java | 9 +++ ...rInboundPlanConversionCoordinatorTest.java | 2 +- .../service/InboundPlanPaginationTest.java | 4 +- .../service/LastlegOrderServiceTest.java | 5 +- .../order/client/FirstlegInboundClient.java | 7 ++ .../InternalOrderCargoController.java | 24 +++++++ .../PlatformUnifiedOrderController.java | 3 +- .../service/OrderApplicationService.java | 53 +++++++++++++++ .../src/main/resources/openapi/order-v1.yaml | 28 ++++++++ .../InternalOrderCargoControllerTest.java | 1 + .../OrderRemarksSynchronizationTest.java | 53 +++++++++++++++ 43 files changed, 621 insertions(+), 52 deletions(-) create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/dto/OrderRemarksSync.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/service/OrderRemarksSyncService.java create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/service/OrderRemarksSyncServiceTest.java create mode 100644 order/src/test/java/com/baiyue/order/service/OrderRemarksSynchronizationTest.java diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index e40fd061b..03d53351d 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -444,6 +444,8 @@ CREATE TABLE ll_dispatch_item ( contact_name varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '私人仓联系人快照', contact_phone varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '私人仓联系电话快照', package_count int NOT NULL DEFAULT '0' COMMENT '箱数', + gross_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '毛重(千克)', + volume_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '材积重(千克)', volume_cbm decimal(18,4) NOT NULL DEFAULT '0.0000' COMMENT '体积(立方米)', source_stage varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据来源阶段', carton_marks varchar(4000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '换标箱唛映射快照', @@ -1674,6 +1676,9 @@ CREATE TABLE ll_outbound_order ( status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'WAIT_OUTBOUND' COMMENT '业务状态', package_count int NOT NULL DEFAULT '0' COMMENT '箱数', pallet_count int NOT NULL DEFAULT '0' COMMENT '托盘数', + gross_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '毛重(千克)', + volume_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '材积重(千克)', + volume_cbm decimal(18,4) NOT NULL DEFAULT '0.0000' COMMENT '体积(立方米)', idempotency_key varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务幂等键', created_by varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人ID', created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -1699,6 +1704,9 @@ CREATE TABLE ll_outbound_pallet_item ( destination_warehouse_code varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的仓编码快照', destination_warehouse_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的仓名称快照', package_count int NOT NULL DEFAULT '0' COMMENT '箱数', + gross_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '毛重(千克)', + volume_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '材积重(千克)', + volume_cbm decimal(18,4) NOT NULL DEFAULT '0.0000' COMMENT '体积(立方米)', picked_package_count int NOT NULL DEFAULT '0' COMMENT '已拣箱数', status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '一件代发SKU', diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index e555ecbb1..0355d2175 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -36,7 +36,7 @@ | 财务费用来源快照 | `controller/InternalFinanceFeeSourceController.java`;入库费用与柜次费用分别按来源 ID、版本向 Finance 提供不可变快照 | | 柜次费用明细与跨客户分摊组 | `controller/PlatformContainerFeeDetailController.java`、`service/ContainerFeeDetailService.java`、`dto/ContainerFeeDetailDtos.java`;跨客户公共柜费使用 `allocation-groups` 按柜内真实客户和订单原子分摊,入仓 ID/入仓码同时填写或同时留空;各行金额合计必须等于原始柜费,审核、驳回、调整、作废均整组处理,单行入口拒绝分摊组;契约 `src/main/resources/openapi/container-fees-v1.yaml`;Admin 消费端见 `../../apps/admin-web/src/modules/loading-lists/api/containerFees.ts` | | 财务整柜成本分摊快照 | `loading/controller/InternalFinanceCostSnapshotController.java`、`loading/service/FinanceCostSnapshotService.java` | -| 分柜、预装、真实柜号后补与正式装柜清单生命周期 | `split/controller/PlatformSplitPreloadController.java`、`controller/PlatformLoadingListController.java`、`service/LoadingListService.java`;契约 `src/main/resources/openapi/loading-lists-v1.yaml`,详情阶段先查 `/loading-plans/{planId}/lifecycle`。创建柜次时原子生成唯一预装计划,加入入仓码明细时同步创建入库预装聚合与关联,不设推送步骤;同一 `planId` 在 `EDITING/LOADING` 返回 `PRELOAD`,`COMPLETED` 返回 `LOADING_LIST`。订舱关联、预装确认和 PDA task 创建不依赖真实集装箱号;可在 `EDITING/LOADING` 首次补录,最终 PDA 完成前必须存在;已有值纠正仅限 `EDITING` 且任务未领取、无物理作业事实 | +| 分柜、预装、真实柜号后补与正式装柜清单生命周期 | `split/controller/PlatformSplitPreloadController.java`、`controller/PlatformLoadingListController.java`、`service/LoadingListService.java`;契约 `src/main/resources/openapi/loading-lists-v1.yaml`,详情阶段先查 `/loading-plans/{planId}/lifecycle`。创建柜次时原子生成唯一预装计划,加入入仓码明细时同步创建入库预装聚合与关联,不设推送步骤;同一 `planId` 在 `EDITING/LOADING` 返回 `PRELOAD`,`COMPLETED` 返回 `LOADING_LIST`。订单协作备注以 Order 为权威,`OrderRemarksSyncService` 负责订单列表与全部未冻结预装明细双向同步,完成装柜后保留冻结快照。订舱关联、预装确认和 PDA task 创建不依赖真实集装箱号;可在 `EDITING/LOADING` 首次补录,最终 PDA 完成前必须存在;已有值纠正仅限 `EDITING` 且任务未领取、无物理作业事实 | | 同柜不可拆组 | `loading/coload/controller/PlatformColoadGroupController.java`、`service/ColoadGroupService.java`、`persistence/ColoadGroupMapper.java`;契约 `src/main/resources/openapi/coload-groups-v1.yaml`。请求按订单 ID 建组,服务端冻结每票全部 `WAIT_SPLIT` 可用入仓片段与满箱数;仅要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均保持整组原子 | | 货柜与全局智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`client/PendingReviewOrderClient.java`、`loading/`;全局候选合并本服务履约投影与 Order `/internal/order/v1/pending-review-orders` 权威待审核页,工作台柜次从 `/inbound-candidates`、`/containers` 进入;普通箱数、待审核占位、同柜组和全部未拣移除分别走 `/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,明细规格拆分留在服务端 | | 货柜资料、PDA 证据与业务总览 | `controller/PlatformContainerController.java` 的 `/{id}/overview`、`/{id}/documents/{documentType}`,聚合 `mapper/ContainerOverviewMapper.java`;契约 `src/main/resources/openapi/phase3-container-overview-v1.yaml`,四类资料固定为目的港清关、国内出口报关、DO、PU 的当前唯一版本 | diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java index 7c9c3405f..16631c501 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java @@ -5,6 +5,8 @@ import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import java.math.BigDecimal; import java.util.List; @@ -36,10 +38,21 @@ public interface OrderCargoClient { @RequestHeader("X-Internal-Service-Token") String token, @PathVariable long tenantId, @PathVariable long orderId); + @PutMapping("/{tenantId}/{orderId}/remarks") + ApiResponse updateRemarks( + @RequestHeader("X-Internal-Service-Token") String token, + @PathVariable long tenantId, @PathVariable long orderId, + @RequestBody RemarksUpdateRequest request); + record MeasurementDeclarationSnapshot( String referenceId, BigDecimal originalGrossWeightKg, BigDecimal originalVolumeCbm, String defaultActualGoodsName) {} + record RemarksUpdateRequest(String internalRemark, String warehouseRemark, + String customerServiceRemark, long operatorId) {} + record RemarksUpdateResponse(int version, String customerRemark, String internalRemark, + String warehouseRemark, String customerServiceRemark) {} + record CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, Integer originalPackageCount, BigDecimal originalGrossWeightKg, BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalInboundOrderController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalInboundOrderController.java index e32e176a7..24b464e85 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalInboundOrderController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalInboundOrderController.java @@ -12,7 +12,9 @@ import com.baiyue.firstleg.service.InboundOrderCreationService; import com.baiyue.firstleg.service.InboundCartonLabelService; import com.baiyue.firstleg.service.UnifiedInboundQueryService; import com.baiyue.firstleg.service.OrderDeclarationSyncService; +import com.baiyue.firstleg.service.OrderRemarksSyncService; import com.baiyue.firstleg.dto.OrderDeclarationSync; +import com.baiyue.firstleg.dto.OrderRemarksSync; import com.baiyue.firstleg.dto.UnifiedInboundDetail; import com.baiyue.firstleg.dto.UnifiedInboundQuery; import com.baiyue.firstleg.dto.UnifiedInboundRequest; @@ -43,6 +45,8 @@ public class InternalInboundOrderController { private UnifiedInboundQueryService unifiedQueryService; @Resource private OrderDeclarationSyncService declarationSync; + @Resource + private OrderRemarksSyncService remarksSync; @Value("${baiyue.security.internal-service-token}") private String internalToken; private byte[] expectedToken; @@ -127,6 +131,14 @@ public class InternalInboundOrderController { return ApiResponse.success(null); } + @PostMapping("/order-remarks-updated") + public ApiResponse updateOrderRemarks( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @Valid @RequestBody OrderRemarksSync.Command command) { + requireToken(token); + return ApiResponse.success(remarksSync.sync(command)); + } + @PostMapping("/unified-orders/query") public ApiResponse> unifiedPage( @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/OrderRemarksSync.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/OrderRemarksSync.java new file mode 100644 index 000000000..e12d90a7e --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/OrderRemarksSync.java @@ -0,0 +1,22 @@ +package com.baiyue.firstleg.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; + +public final class OrderRemarksSync { + private OrderRemarksSync() { + } + + public record Command( + @Positive long tenantId, + @Positive long orderId, + @Size(max = 1000) String customerRemark, + @Size(max = 1000) String internalRemark, + @Size(max = 1000) String warehouseRemark, + @Size(max = 1000) String customerServiceRemark, + @NotNull @Positive Long operatorId, + @Size(max = 128) String operatorName + ) { + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java index 0edfde3e7..5ad87bae9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java @@ -1,5 +1,5 @@ package com.baiyue.firstleg.mapper; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import org.apache.ibatis.annotations.*; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import org.apache.ibatis.annotations.*; import java.util.List; @Mapper public interface LoadingPlanItemMapper extends BaseMapper { @Update(""" UPDATE fl_loading_plan_item SET deleted=1,removed_by=#{operatorId},removed_by_name=#{operatorName}, @@ -30,4 +30,69 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl @Param("customerServiceRemark") String customerServiceRemark, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); + + @Select(""" + SELECT DISTINCT i.loading_plan_id + FROM fl_loading_plan_item i + JOIN fl_loading_plan p ON p.id=i.loading_plan_id AND p.plan_status IN ('EDITING','LOADING') + WHERE i.account_id=#{tenantId} AND i.order_id=#{orderId} AND i.deleted=0 AND i.frozen_at IS NULL + AND (#{excludedItemId} IS NULL OR i.id<>#{excludedItemId}) + AND NOT (i.customer_remark <=> #{customerRemark} + AND i.internal_remark <=> #{internalRemark} + AND i.warehouse_remark <=> #{warehouseRemark} + AND i.customer_service_remark <=> #{customerServiceRemark}) + """) + List findMutablePlanIdsWithDifferentRemarks(@Param("tenantId") long tenantId, + @Param("orderId") long orderId, + @Param("customerRemark") String customerRemark, + @Param("internalRemark") String internalRemark, + @Param("warehouseRemark") String warehouseRemark, + @Param("customerServiceRemark") String customerServiceRemark, + @Param("excludedItemId") Long excludedItemId); + + @Update(""" + UPDATE fl_loading_plan_item i + JOIN fl_loading_plan p ON p.id=i.loading_plan_id AND p.plan_status IN ('EDITING','LOADING') + SET i.customer_remark=#{customerRemark}, i.internal_remark=#{internalRemark}, i.warehouse_remark=#{warehouseRemark}, + i.customer_service_remark=#{customerServiceRemark}, i.updated_by=#{operatorId}, + i.updated_by_name=#{operatorName}, i.updated_at=CURRENT_TIMESTAMP(3), i.version=i.version+1 + WHERE i.account_id=#{tenantId} AND i.order_id=#{orderId} AND i.deleted=0 AND i.frozen_at IS NULL + AND (#{excludedItemId} IS NULL OR i.id<>#{excludedItemId}) + AND NOT (i.customer_remark <=> #{customerRemark} + AND i.internal_remark <=> #{internalRemark} + AND i.warehouse_remark <=> #{warehouseRemark} + AND i.customer_service_remark <=> #{customerServiceRemark}) + """) + int syncMutableOrderRemarks(@Param("tenantId") long tenantId, @Param("orderId") long orderId, + @Param("customerRemark") String customerRemark, + @Param("internalRemark") String internalRemark, + @Param("warehouseRemark") String warehouseRemark, + @Param("customerServiceRemark") String customerServiceRemark, + @Param("operatorId") long operatorId, + @Param("operatorName") String operatorName, + @Param("excludedItemId") Long excludedItemId); + + @Select(""" + SELECT DISTINCT i.loading_plan_id + FROM fl_loading_plan_item i + JOIN fl_loading_plan p ON p.id=i.loading_plan_id AND p.plan_status IN ('EDITING','LOADING') + WHERE i.account_id=#{tenantId} AND i.order_id=#{orderId} AND i.deleted=0 AND i.frozen_at IS NULL + AND NOT (i.customer_remark <=> #{customerRemark}) + """) + List findMutablePlanIdsWithDifferentCustomerRemark(@Param("tenantId") long tenantId, + @Param("orderId") long orderId, + @Param("customerRemark") String customerRemark); + + @Update(""" + UPDATE fl_loading_plan_item i + JOIN fl_loading_plan p ON p.id=i.loading_plan_id AND p.plan_status IN ('EDITING','LOADING') + SET i.customer_remark=#{customerRemark}, i.updated_by=#{operatorId}, i.updated_by_name=#{operatorName}, + i.updated_at=CURRENT_TIMESTAMP(3), i.version=i.version+1 + WHERE i.account_id=#{tenantId} AND i.order_id=#{orderId} AND i.deleted=0 AND i.frozen_at IS NULL + AND NOT (i.customer_remark <=> #{customerRemark}) + """) + int syncMutableOrderCustomerRemark(@Param("tenantId") long tenantId, @Param("orderId") long orderId, + @Param("customerRemark") String customerRemark, + @Param("operatorId") long operatorId, + @Param("operatorName") String operatorName); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanMapper.java index 59d07e48f..0e1e60f07 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPlanMapper.java @@ -1,5 +1,5 @@ package com.baiyue.firstleg.mapper; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanEntity; import org.apache.ibatis.annotations.*; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanEntity; import org.apache.ibatis.annotations.*; import java.util.List; @Mapper public interface LoadingPlanMapper extends BaseMapper { @Update(""" UPDATE fl_loading_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName}, @@ -13,4 +13,16 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl WHERE id=#{planId} AND plan_status='EDITING' """) int startPicking(@Param("planId") long planId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName); + + @Update(""" + + """) + int touchForOrderRemarks(@Param("planIds") List planIds, + @Param("operatorId") long operatorId, + @Param("operatorName") String operatorName); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java index 1230bc021..0134d90ad 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java @@ -25,6 +25,7 @@ import java.util.Objects; public class OrderDeclarationSyncService { @Resource private OrderDeclarationSyncMapper mapper; @Resource private ObjectMapper json; + @Resource private OrderRemarksSyncService remarksSync; @Transactional(readOnly = true) public ChannelChangeValidationResponse validateChannelChange(ChannelChangeValidationRequest request) { @@ -86,6 +87,8 @@ public class OrderDeclarationSyncService { normalizeChannel(event.channelCode()), event.channelName(), event.operatorId()) != 1) { throw conflict("Firstleg 入库申报快照已发生变化"); } + remarksSync.syncCustomerRemark(event.tenantId(), event.orderId(), event.customerRemark(), + event.operatorId(), "order-declaration"); } private void recordEvent(UpdatedEvent event, String hash) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/OrderRemarksSyncService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/OrderRemarksSyncService.java new file mode 100644 index 000000000..0e2eb434a --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/OrderRemarksSyncService.java @@ -0,0 +1,57 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.firstleg.dto.OrderRemarksSync; +import com.baiyue.firstleg.mapper.LoadingPlanItemMapper; +import com.baiyue.firstleg.mapper.LoadingPlanMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class OrderRemarksSyncService { + @Resource private LoadingPlanItemMapper items; + @Resource private LoadingPlanMapper plans; + + @Transactional + public int sync(OrderRemarksSync.Command command) { + return sync(command, null, null); + } + + @Transactional + public int sync(OrderRemarksSync.Command command, Long alreadyTouchedPlanId) { + return sync(command, alreadyTouchedPlanId, null); + } + + @Transactional + public int sync(OrderRemarksSync.Command command, Long alreadyTouchedPlanId, Long excludedItemId) { + List affectedPlanIds = items.findMutablePlanIdsWithDifferentRemarks( + command.tenantId(), command.orderId(), command.customerRemark(), command.internalRemark(), command.warehouseRemark(), + command.customerServiceRemark(), excludedItemId); + if (affectedPlanIds.isEmpty()) return 0; + int updated = items.syncMutableOrderRemarks(command.tenantId(), command.orderId(), + command.customerRemark(), command.internalRemark(), command.warehouseRemark(), command.customerServiceRemark(), + command.operatorId(), command.operatorName(), excludedItemId); + if (updated > 0) { + List plansToTouch = affectedPlanIds.stream() + .filter(planId -> !planId.equals(alreadyTouchedPlanId)).toList(); + if (!plansToTouch.isEmpty()) { + plans.touchForOrderRemarks(plansToTouch, command.operatorId(), command.operatorName()); + } + } + return updated; + } + + @Transactional + public int syncCustomerRemark(long tenantId, long orderId, String customerRemark, + long operatorId, String operatorName) { + List affectedPlanIds = items.findMutablePlanIdsWithDifferentCustomerRemark( + tenantId, orderId, customerRemark); + if (affectedPlanIds.isEmpty()) return 0; + int updated = items.syncMutableOrderCustomerRemark( + tenantId, orderId, customerRemark, operatorId, operatorName); + if (updated > 0) plans.touchForOrderRemarks(affectedPlanIds, operatorId, operatorName); + return updated; + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index 63791e182..5b9879d81 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -6,6 +6,7 @@ import com.baiyue.common.core.api.PageResult; import com.baiyue.common.core.exception.BizException; import com.baiyue.common.audit.BusinessAudit; import com.baiyue.firstleg.dto.SmartLoadingDtos.*; +import com.baiyue.firstleg.dto.OrderRemarksSync; import com.baiyue.firstleg.domain.SmartLoadingChannelGroupCatalog; import com.baiyue.firstleg.client.OrderCargoClient; import com.baiyue.firstleg.client.PendingReviewOrderClient; @@ -56,6 +57,7 @@ public class SmartLoadingService { @Resource private FbaPricingService fbaPricingService; @Resource private PriceBookQueryUseCase priceBooks; @Resource private FirstlegDocumentNumberGenerator documentNumbers; + @Resource private OrderRemarksSyncService orderRemarksSync; @Resource private ObjectMapper objectMapper; @Value("${baiyue.security.internal-service-token:}") private String internalToken; public SmartLoadingService() { } @@ -1099,6 +1101,9 @@ public class SmartLoadingService { LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName); LoadingPlanItemEntity item = repository.item(planId, itemId); if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在"); + if (item.getVersion() != request.expectedItemVersion()) { + throw conflict("装柜计划或明细已被其他人修改,请刷新后重试"); + } if (Objects.equals(item.getInternalRemark(), internalRemark) && Objects.equals(item.getWarehouseRemark(), warehouseRemark) && Objects.equals(item.getCustomerServiceRemark(), customerServiceRemark)) { @@ -1109,16 +1114,30 @@ public class SmartLoadingService { "version", item.getVersion())); try { touch(plan, operatorId, operatorName); + ApiResponse response = orderCargoClient.updateRemarks( + internalToken, item.getAccountId(), item.getOrderId(), + new OrderCargoClient.RemarksUpdateRequest(internalRemark, warehouseRemark, + customerServiceRemark, operatorId)); + if (response == null || !response.successful() || response.data() == null) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, + response == null ? "订单备注服务暂不可用" : response.message()); + } if (repository.updateRemarks(planId, itemId, request.expectedPlanVersion() + 1, - request.expectedItemVersion(), internalRemark, warehouseRemark, customerServiceRemark, - operatorId, operatorName) != 1) { + request.expectedItemVersion(), response.data().internalRemark(), response.data().warehouseRemark(), + response.data().customerServiceRemark(), operatorId, operatorName) != 1) { throw conflict("装柜计划或明细已被其他人修改,请刷新后重试"); } + orderRemarksSync.sync(new OrderRemarksSync.Command(item.getAccountId(), item.getOrderId(), + response.data().customerRemark(), + response.data().internalRemark(), response.data().warehouseRemark(), + response.data().customerServiceRemark(), operatorId, operatorName), planId, itemId); } catch (DataAccessException exception) { if (hasSqlState(exception, "45000")) { throw conflict("装柜计划或明细状态已变化,备注未保存,请刷新后重试"); } throw exception; + } catch (FeignException exception) { + throw unavailable(); } history(planId, itemId, "REMARKS", plan.getPlanStatus(), plan.getPlanStatus(), operation, before, json(values("internalRemark", internalRemark, "warehouseRemark", warehouseRemark, diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index ec999ea0b..226880cc9 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -292,6 +292,32 @@ paths: schema: {$ref: '#/components/schemas/OrderDeclarationUpdatedEvent'} responses: '200': {description: 成功、幂等重放或旧版本已忽略;冲突时响应体 code=A0409} + /internal/firstleg/v1/inbounds/order-remarks-updated: + post: + summary: 同步 Order 权威协作备注到未冻结预装明细 + description: >- + 仅接受内部服务令牌。按客户与订单关联全部 EDITING/LOADING 且未冻结的预装明细, + 同步内部、仓库和客服备注并递增受影响计划与明细版本;已完成装柜的冻结快照不修改。 + security: + - internalServiceToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [tenantId, orderId, operatorId] + properties: + tenantId: {type: integer, format: int64, minimum: 1} + orderId: {type: integer, format: int64, minimum: 1} + customerRemark: {type: string, nullable: true, maxLength: 1000} + internalRemark: {type: string, nullable: true, maxLength: 1000} + warehouseRemark: {type: string, nullable: true, maxLength: 1000} + customerServiceRemark: {type: string, nullable: true, maxLength: 1000} + operatorId: {type: integer, format: int64, minimum: 1} + operatorName: {type: string, nullable: true, maxLength: 128} + responses: + '200': {description: data 返回已同步的预装明细数量} /internal/firstleg/v1/inbounds/unified-orders/query: post: summary: 按统一订单条件分页查询履约摘要(Order 内部) diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java index 93c24882d..5cd1aec90 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java @@ -7,6 +7,7 @@ import com.baiyue.firstleg.service.InboundOrderCreationService; import com.baiyue.firstleg.service.InboundCartonLabelService; import com.baiyue.firstleg.service.UnifiedInboundQueryService; import com.baiyue.firstleg.service.OrderDeclarationSyncService; +import com.baiyue.firstleg.service.OrderRemarksSyncService; import com.baiyue.firstleg.dto.UnifiedInboundQuery; import com.baiyue.firstleg.dto.OrderDeclarationSync; import com.baiyue.common.core.api.PageResult; @@ -36,6 +37,8 @@ class InternalInboundOrderReferenceControllerTest { private UnifiedInboundQueryService unifiedQueryService; @MockBean private OrderDeclarationSyncService declarationSync; + @MockBean + private OrderRemarksSyncService remarksSync; @Resource private InternalInboundOrderController controller; diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/OrderDeclarationSyncServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/OrderDeclarationSyncServiceTest.java index 98445bcd8..50cf520ef 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/OrderDeclarationSyncServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/OrderDeclarationSyncServiceTest.java @@ -35,6 +35,7 @@ class OrderDeclarationSyncServiceTest { } @MockBean private OrderDeclarationSyncMapper mapper; + @MockBean private OrderRemarksSyncService remarksSync; @Resource private OrderDeclarationSyncService service; @Resource private ObjectMapper json; diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/OrderRemarksSyncServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/OrderRemarksSyncServiceTest.java new file mode 100644 index 000000000..01ea81046 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/OrderRemarksSyncServiceTest.java @@ -0,0 +1,50 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.firstleg.dto.OrderRemarksSync; +import com.baiyue.firstleg.mapper.LoadingPlanItemMapper; +import com.baiyue.firstleg.mapper.LoadingPlanMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringJUnitConfig(OrderRemarksSyncService.class) +class OrderRemarksSyncServiceTest { + @MockBean private LoadingPlanItemMapper items; + @MockBean private LoadingPlanMapper plans; + + @jakarta.annotation.Resource private OrderRemarksSyncService service; + + @Test + void updatesAllMutableOrderItemsAndTouchesAffectedPlans() { + var command = new OrderRemarksSync.Command(10L, 20L, "客户", "内部", "仓库", "客服", 30L, "操作人"); + when(items.findMutablePlanIdsWithDifferentRemarks( + 10L, 20L, "客户", "内部", "仓库", "客服", null)).thenReturn(List.of(101L, 102L)); + when(items.syncMutableOrderRemarks( + 10L, 20L, "客户", "内部", "仓库", "客服", 30L, "操作人", null)).thenReturn(3); + + assertThat(service.sync(command)).isEqualTo(3); + + verify(plans).touchForOrderRemarks(List.of(101L, 102L), 30L, "操作人"); + } + + @Test + void leavesFrozenOrAlreadyMatchingPlansUntouched() { + var command = new OrderRemarksSync.Command(10L, 20L, null, null, null, null, 30L, null); + when(items.findMutablePlanIdsWithDifferentRemarks( + 10L, 20L, null, null, null, null, null)).thenReturn(List.of()); + + assertThat(service.sync(command)).isZero(); + + verify(items, never()).syncMutableOrderRemarks( + 10L, 20L, null, null, null, null, 30L, null, null); + verify(plans, never()).touchForOrderRemarks(org.mockito.ArgumentMatchers.anyList(), + org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.nullable(String.class)); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java index 11fb0cca6..e1065048f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java @@ -102,6 +102,7 @@ class SmartLoadingServiceTest { @MockBean private PriceBookQueryUseCase priceBooks; @MockBean private FirstlegDocumentNumberGenerator documentNumbers; @MockBean private DropshipPalletizationMapper dropshipPallets; + @MockBean private OrderRemarksSyncService orderRemarksSync; @Resource private SmartLoadingService service; @TestConfiguration @@ -119,6 +120,12 @@ class SmartLoadingServiceTest { when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn( ApiResponse.success(new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 5, new BigDecimal("10"), new BigDecimal("2")))); + when(cargoClient.updateRemarks(anyString(), anyLong(), anyLong(), any())) + .thenAnswer(invocation -> { + OrderCargoClient.RemarksUpdateRequest request = invocation.getArgument(3); + return ApiResponse.success(new OrderCargoClient.RemarksUpdateResponse(2, null, + request.internalRemark(), request.warehouseRemark(), request.customerServiceRemark())); + }); when(fbaPricingService.recalculateUnlockedContainerCustomerFees(anyLong(), anyLong(), anyString())) .thenReturn(new FbaPricingService.ContainerFeeRecalculationResult( FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0)); @@ -1955,6 +1962,7 @@ class SmartLoadingServiceTest { private static LoadingPlanItemEntity item(long id, long account, long detail, int boxes) { LoadingPlanItemEntity value = new LoadingPlanItemEntity(); value.setId(id); value.setLoadingPlanId(1L); value.setAccountId(account); + value.setOrderId(30L); value.setOrderNo("O30"); value.setInboundId(10L); value.setInboundDetailId(detail); value.setPackageCount(boxes); value.setVersion(0); value.setDeleted(false); return value; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java index 8949e7696..c150e4407 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java @@ -16,7 +16,9 @@ public final class DeliveryResponses { List deliveries, boolean replayed) {} public record OutboundRow(Long id, String outboundNo, String dispatchNo, String overseasWarehouseId, String lxCodes, - String status, int palletCount, int packageCount, LocalDateTime createdAt, String outboundType) {} + String status, int palletCount, int packageCount, BigDecimal grossWeightKg, + BigDecimal volumeWeightKg, BigDecimal volumeCbm, + LocalDateTime createdAt, String outboundType) {} public record DeliveryRow(Long id, String customerId, String customerName, String deliveryNo, String outboundNo, String dispatchNo, String overseasWarehouseId, @@ -51,7 +53,9 @@ public final class DeliveryResponses { /** palletNo is the warehouse-authoritative physical pallet code after inbound assignment. */ public record OutboundPalletRow(Long id, String palletNo, String cartonMarks, String inboundCode, String destinationWarehouseCode, String destinationWarehouseName, - String skuCode, int packageCount, int pickedPackageCount, String status) {} + String skuCode, int packageCount, BigDecimal grossWeightKg, + BigDecimal volumeWeightKg, BigDecimal volumeCbm, + int pickedPackageCount, String status) {} public record OutboundDetail(OutboundRow outbound, List deliveries, List pallets) {} public record OneClickCandidate(Long inventoryPositionId, String orderNo, String customerId, String customerName, String overseasWarehouseId, String overseasWarehouseName, String palletNo, String inboundCode, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundOrder.java index 0c088baaf..e598fdf53 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundOrder.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundOrder.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; +import java.math.BigDecimal; import java.time.LocalDateTime; /** Outbound document created when a dispatch is transferred to delivery. */ @@ -24,6 +25,9 @@ public class OutboundOrder { private String status; private Integer packageCount; private Integer palletCount; + private BigDecimal grossWeightKg; + private BigDecimal volumeWeightKg; + private BigDecimal volumeCbm; private String idempotencyKey; private String requestHash; private String createdBy; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPalletItem.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPalletItem.java index 658c97bfa..6c69084c3 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPalletItem.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPalletItem.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; +import java.math.BigDecimal; import java.time.LocalDateTime; /** Pallet-level immutable snapshot belonging to one outbound document. */ @@ -21,6 +22,9 @@ public class OutboundPalletItem { private String destinationWarehouseCode; private String destinationWarehouseName; private Integer packageCount; + private BigDecimal grossWeightKg; + private BigDecimal volumeWeightKg; + private BigDecimal volumeCbm; private Integer pickedPackageCount; private String status; private String skuCode; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java index 157f915ae..eed41070d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java @@ -165,7 +165,11 @@ public class DeliveryTransferService { outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND"); outbound.setOutboundType("DISPATCH_DELIVERY"); outbound.setLxCodes(resolveLxCodes(customerId, items)); outbound.setPackageCount(items.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); - outbound.setPalletCount(items.size()); outbound.setIdempotencyKey(request.idempotencyKey().trim()); + outbound.setPalletCount(items.size()); + outbound.setGrossWeightKg(sum(items, DispatchItem::getGrossWeightKg)); + outbound.setVolumeWeightKg(sum(items, DispatchItem::getVolumeWeightKg)); + outbound.setVolumeCbm(sum(items, DispatchItem::getVolumeCbm)); + outbound.setIdempotencyKey(request.idempotencyKey().trim()); outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); try { outboundOrders.insert(outbound); @@ -190,7 +194,9 @@ public class DeliveryTransferService { item.setDestinationWarehouseId(source.getDestinationWarehouseId()); item.setDestinationWarehouseCode(source.getDestinationWarehouseCode()); item.setDestinationWarehouseName(source.getDestinationWarehouseName()); - item.setPackageCount(Objects.requireNonNullElse(source.getPackageCount(), 0)); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); + item.setPackageCount(Objects.requireNonNullElse(source.getPackageCount(), 0)); + item.setGrossWeightKg(source.getGrossWeightKg()); item.setVolumeWeightKg(source.getVolumeWeightKg()); + item.setVolumeCbm(source.getVolumeCbm()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); if (StringUtils.hasText(source.getCartonMarks())) { item.setCartonMarks(source.getCartonMarks()); } else if (source.getWarehousePalletId() != null) { @@ -236,8 +242,8 @@ public class DeliveryTransferService { delivery.setSellerName(trim(request.sellerName())); delivery.setPlannedDeliveryAt(request.plannedDeliveryAt()); delivery.setPackageCount(groupItems.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); delivery.setPalletCount(groupItems.size()); - delivery.setTotalVolumeCbm(groupItems.stream().map(DispatchItem::getVolumeCbm).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); - delivery.setTotalWeightKg(totalWeight(customerId, groupItems)); delivery.setExpectedArrivalAt(dispatch.getEstimatedArrivalAt()); delivery.setRemark(trim(request.remark())); + delivery.setTotalVolumeCbm(sum(groupItems, DispatchItem::getVolumeCbm)); + delivery.setTotalWeightKg(sum(groupItems, DispatchItem::getGrossWeightKg)); delivery.setExpectedArrivalAt(dispatch.getEstimatedArrivalAt()); delivery.setRemark(trim(request.remark())); delivery.setIdempotencyKey(childKey(request.idempotencyKey(), "DEST-" + groupIndex++)); delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now); deliveryOrders.insert(delivery); recordOperation(customerId, delivery.getId(), operatorId, "CREATE_DELIVERY", "按目的FBA仓生成派送单", now); @@ -1171,11 +1177,12 @@ public class DeliveryTransferService { d.getDeliveryNo(), d.getStatus(), d.getDestinationWarehouseId(), d.getDestinationWarehouseCode(), d.getDestinationWarehouseName(), value(d.getPalletCount()), value(d.getPackageCount()))).toList(), replayed); } - private DeliveryResponses.OutboundRow outboundRow(OutboundOrder o) { return new DeliveryResponses.OutboundRow(o.getId(), o.getOutboundNo(), o.getDispatchNo(), o.getOverseasWarehouseId(), o.getLxCodes(), o.getStatus(), o.getPalletCount(), o.getPackageCount(), o.getCreatedAt(), o.getOutboundType()); } + private DeliveryResponses.OutboundRow outboundRow(OutboundOrder o) { return new DeliveryResponses.OutboundRow(o.getId(), o.getOutboundNo(), o.getDispatchNo(), o.getOverseasWarehouseId(), o.getLxCodes(), o.getStatus(), o.getPalletCount(), o.getPackageCount(), o.getGrossWeightKg(), o.getVolumeWeightKg(), o.getVolumeCbm(), o.getCreatedAt(), o.getOutboundType()); } private DeliveryResponses.OutboundPalletRow outboundPalletRow(OutboundPalletItem item) { return new DeliveryResponses.OutboundPalletRow(item.getId(), item.getPalletNo(), item.getCartonMarks(), item.getInboundCode(), item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), - item.getSkuCode(), value(item.getPackageCount()), value(item.getPickedPackageCount()), item.getStatus()); + item.getSkuCode(), value(item.getPackageCount()), item.getGrossWeightKg(), item.getVolumeWeightKg(), + item.getVolumeCbm(), value(item.getPickedPackageCount()), item.getStatus()); } private DeliveryResponses.OneClickCandidate oneClickCandidate(DropshipOutboundCandidate candidate) { return new DeliveryResponses.OneClickCandidate(candidate.getInventoryPositionId(), candidate.getOrderNo(), candidate.getCustomerId(), candidate.getCustomerName(), @@ -1235,26 +1242,9 @@ public class DeliveryTransferService { Math.addExact(quantity, other.quantity)); } } - private BigDecimal totalWeight(String customerId, List items) { - List palletIds = items.stream().map(DispatchItem::getWarehousePalletId) - .filter(Objects::nonNull).distinct().toList(); - List transferPallets = palletIds.isEmpty() ? List.of() - : warehouseTransferPallets.selectList(new LambdaQueryWrapper() - .eq(WarehouseTransferPallet::getCustomerId, customerId) - .in(WarehouseTransferPallet::getWarehousePalletId, palletIds)); - Set measuredPalletIds = transferPallets.stream() - .map(WarehouseTransferPallet::getWarehousePalletId).collect(java.util.stream.Collectors.toSet()); - BigDecimal measuredWeight = transferPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg) - .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); - List orderIds = items.stream() - .filter(item -> item.getWarehousePalletId() == null || !measuredPalletIds.contains(item.getWarehousePalletId())) - .map(DispatchItem::getOrderId).filter(Objects::nonNull).distinct().toList(); - if (orderIds.isEmpty()) return measuredWeight; - BigDecimal orderWeight = tailOrderDetails.selectList(new LambdaQueryWrapper() - .eq(LastlegOrderDetail::getCustomerId, customerId).in(LastlegOrderDetail::getOrderId, orderIds)) - .stream().map(LastlegOrderDetail::getGrossWeightKg).filter(Objects::nonNull) - .reduce(BigDecimal.ZERO, BigDecimal::add); - return measuredWeight.add(orderWeight); + private static BigDecimal sum(List items, + java.util.function.Function getter) { + return items.stream().map(getter).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); } private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; } private String hash(Object value) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java index 7de25f0a8..b487c19b0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java @@ -17,6 +17,7 @@ public record DispatchResponse(Long id, String dispatchNo, String vehiclePlate, String overseasWarehouseId, String overseasWarehouseName, String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode, String destinationWarehouseName, String fbaWarehouseCode, int packageCount, + BigDecimal grossWeightKg, BigDecimal volumeWeightKg, BigDecimal volumeCbm, String sourceStage) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItem.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItem.java index d3e4a1505..df89323c0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItem.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItem.java @@ -36,6 +36,8 @@ public class DispatchItem { private String contactName; private String contactPhone; private Integer packageCount; + private BigDecimal grossWeightKg; + private BigDecimal volumeWeightKg; private BigDecimal volumeCbm; /** PLAN for planned pallets; RECEIVED for warehouse/entity pallets. */ private String sourceStage; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index 23a900d18..8336fa19c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -410,6 +410,8 @@ public class DispatchService { item.setContactName(candidate.contactName()); item.setContactPhone(candidate.contactPhone()); item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0)); + item.setGrossWeightKg(Objects.requireNonNullElse(candidate.grossWeightKg(), BigDecimal.ZERO)); + item.setVolumeWeightKg(Objects.requireNonNullElse(candidate.volumeWeightKg(), BigDecimal.ZERO)); item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO)); item.setSourceStage(candidate.sourceStage()); item.setCartonMarks(mergeCartonMarks(candidate.cartonMarks(), candidate.relabelCartonMarks())); @@ -457,7 +459,7 @@ public class DispatchService { DispatchOrder dispatch = new DispatchOrder(); dispatch.setId(IdWorker.getId()); dispatch.setCustomerId(customerId); dispatch.setDispatchNo(orderNoGenerator.next("DISPATCH", warehouse, dispatch.getId())); dispatch.setOverseasWarehouseId(warehouse); dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName()); dispatch.setRoute(candidateRoute(selected)); dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo())); dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus())); dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus())); dispatch.setStatus(PENDING_DEPARTURE); dispatch.setItemCount(selected.size()); dispatch.setPalletCount(selected.size()); dispatch.setPackageCount(selected.stream().map(DispatchPalletCandidate::packageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); dispatch.setPickedPackageCount(0); dispatch.setShippedPackageCount(0); dispatch.setCreatedBy(trimToNull(operatorId)); dispatch.setCreatedAt(now); dispatch.setUpdatedAt(now); orders.insert(dispatch); - for (DispatchPalletCandidate candidate : selected) { DispatchItem item = new DispatchItem(); item.setId(IdWorker.getId()); item.setDispatchOrderId(dispatch.getId()); item.setCustomerId(customerId); item.setWarehousePalletId(candidate.warehousePalletId()); item.setOrderId(candidate.orderId()); item.setOverseasWarehouseId(candidate.overseasWarehouseId()); item.setOverseasWarehouseName(candidate.overseasWarehouseName()); item.setOrderNo(candidate.orderNo()); item.setOrderRefs(candidate.orderRefs()); item.setInboundCode(candidate.inboundCode()); item.setPalletNo(candidate.palletNo()); item.setDestinationWarehouseCode(candidate.destinationWarehouseCode()); item.setDestinationWarehouseName(candidate.destinationWarehouseName()); item.setFbaWarehouseCode(candidate.fbaWarehouseCode()); item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0)); item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO)); item.setSourceStage("RELABEL"); item.setCartonMarks(candidate.relabelCartonMarks()); item.setSourceBusinessType("RELABEL_ORDER"); item.setSourceBusinessNo(request.relabelNo()); item.setCreatedAt(now); items.insert(item); } + for (DispatchPalletCandidate candidate : selected) { DispatchItem item = new DispatchItem(); item.setId(IdWorker.getId()); item.setDispatchOrderId(dispatch.getId()); item.setCustomerId(customerId); item.setWarehousePalletId(candidate.warehousePalletId()); item.setOrderId(candidate.orderId()); item.setOverseasWarehouseId(candidate.overseasWarehouseId()); item.setOverseasWarehouseName(candidate.overseasWarehouseName()); item.setOrderNo(candidate.orderNo()); item.setOrderRefs(candidate.orderRefs()); item.setInboundCode(candidate.inboundCode()); item.setPalletNo(candidate.palletNo()); item.setDestinationWarehouseCode(candidate.destinationWarehouseCode()); item.setDestinationWarehouseName(candidate.destinationWarehouseName()); item.setFbaWarehouseCode(candidate.fbaWarehouseCode()); item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0)); item.setGrossWeightKg(Objects.requireNonNullElse(candidate.grossWeightKg(), BigDecimal.ZERO)); item.setVolumeWeightKg(Objects.requireNonNullElse(candidate.volumeWeightKg(), BigDecimal.ZERO)); item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO)); item.setSourceStage("RELABEL"); item.setCartonMarks(candidate.relabelCartonMarks()); item.setSourceBusinessType("RELABEL_ORDER"); item.setSourceBusinessNo(request.relabelNo()); item.setCreatedAt(now); items.insert(item); } DispatchPdaTask task = new DispatchPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(dispatch.getId()); task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING"); task.setWarehouseId(warehouse); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task); if (candidates.markRelabelPreDispatched(customerId, request.relabelNo()) != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单已被其他排车占用"); return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId())); @@ -653,7 +655,8 @@ public class DispatchService { item.getWarehousePalletId(), item.getPalletNo(), item.getOverseasWarehouseId(), item.getOverseasWarehouseName(), item.getOrderNo(), item.getOrderRefs(), item.getInboundCode(), item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), - Objects.requireNonNullElse(item.getPackageCount(), 0), item.getVolumeCbm(), item.getSourceStage())).toList(), + Objects.requireNonNullElse(item.getPackageCount(), 0), item.getGrossWeightKg(), + item.getVolumeWeightKg(), item.getVolumeCbm(), item.getSourceStage())).toList(), bolLineList == null ? List.of() : bolLineList); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java index ee0c3a2da..ede78069f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java @@ -47,6 +47,10 @@ public record LastlegOrderSummary( Integer receivedPackageCount, @Schema(description = "实收减预报箱数;尚未完成收货时为空") Integer packageDifference, + @Schema(description = "头程冻结货物毛重(千克)") + BigDecimal grossWeightKg, + @Schema(description = "头程冻结货物净重(千克)") + BigDecimal netWeightKg, BigDecimal volumeCbm, Integer productQuantity, BigDecimal volumeWeightKg, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java index efb5922a1..4311dbccc 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java @@ -22,6 +22,8 @@ public record InboundPlanListResponse( String customerName, Integer packageCount, Integer pickedPackageCount, + BigDecimal grossWeightKg, + BigDecimal netWeightKg, BigDecimal volumeCbm, Integer palletCount, boolean assigned, @@ -39,7 +41,8 @@ public record InboundPlanListResponse( public InboundPlanListResponse withReceivingWarehouse(String warehouseCode, String warehouseName) { return new InboundPlanListResponse(lineId, planId, planNo, customerId, orderId, inboundCode, lxCode, orderType, overseasWarehouseId, warehouseCode, warehouseName, warehouseId, this.warehouseCode, - this.warehouseName, fbaWarehouseCode, customerName, packageCount, pickedPackageCount, volumeCbm, + this.warehouseName, fbaWarehouseCode, customerName, packageCount, pickedPackageCount, + grossWeightKg, netWeightKg, volumeCbm, palletCount, assigned, mergeAllowed, urgent, status, appointmentNo, appointmentStatus, appointmentAt, expectedLoadingAt, dispatchStatus, createdAt, version); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java index f36ce2706..8bcd713b7 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java @@ -19,6 +19,8 @@ public record InboundPlanResponse( String status, int lineCount, int palletCount, + BigDecimal totalGrossWeightKg, + BigDecimal totalNetWeightKg, BigDecimal totalVolumeCbm, int expectedPackageCount, int pickedPackageCount, @@ -38,7 +40,8 @@ public record InboundPlanResponse( ) { public record Line(Long id, Long orderId, String inboundCode, String fbaWarehouseCode, String destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, - BigDecimal volumeCbm, int requiredPackageCount, int plannedPalletCount) { + BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm, + int requiredPackageCount, int plannedPalletCount) { } public record Pallet(Long id, String palletNo, String destinationWarehouseId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java index 88fa7dc8f..0569b86d0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java @@ -17,6 +17,8 @@ public interface InboundPlanOrderMapper { record ReceivingWarehouseSnapshot(String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName) { } record DetailOrderSnapshot(String orderNo, String customerName, String orderType) { } + record OrderWeightSnapshot(Long orderId, java.math.BigDecimal grossWeightKg, + java.math.BigDecimal netWeightKg) { } record ContainerWarehouseRef(String customerId, Long orderId, String sourceContainerBookingId, String overseasWarehouseId) { } record ContainerPreviewOrderRow( @@ -116,6 +118,17 @@ public interface InboundPlanOrderMapper { DetailOrderSnapshot findDetailOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId); + @Select("") + List findOrderWeights(@Param("customerId") String customerId, + @Param("orderIds") List orderIds); + @Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," + "pallet_merge_flag='MERGED',pallet_count=#{palletCount},updated_at=#{updatedAt} " + "WHERE customer_id=#{customerId} AND id=#{orderId}") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java index d15879ebf..f428897ce 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java @@ -43,6 +43,10 @@ public interface InboundPlanQueryMapper { "l.destination_warehouse_name warehouseName,l.fba_warehouse_code fbaWarehouseCode," + "o.customer_name customerName,l.required_package_count packageCount," + "COALESCE((SELECT SUM(pa.picked_package_count) FROM ll_inbound_plan_pallet_allocation pa WHERE pa.line_id=l.id),0) pickedPackageCount," + + "COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.order_id=l.order_id AND d.customer_id=p.customer_id)," + + "(SELECT i.gross_weight_kg FROM ll_inbound_order i WHERE i.id=l.order_id AND i.customer_id=p.customer_id),0) grossWeightKg," + + "COALESCE((SELECT SUM(d.net_weight_kg) FROM ll_order_detail d WHERE d.order_id=l.order_id AND d.customer_id=p.customer_id)," + + "(SELECT i.net_weight_kg FROM ll_inbound_order i WHERE i.id=l.order_id AND i.customer_id=p.customer_id),0) netWeightKg," + "l.volume_cbm volumeCbm," + "l.planned_pallet_count palletCount,p.assigned_flag assigned,p.merge_required mergeAllowed," + "p.urgent_flag urgent,p.status status,a.appointment_no appointmentNo," + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java index 1af3955b6..31483e4be 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java @@ -1077,6 +1077,16 @@ public class InboundPlanService { } List lines = lineMapper.selectList(new LambdaQueryWrapper() .eq(InboundPlanLine::getPlanId, plan.getId()).orderByAsc(InboundPlanLine::getId)); + Map weights = lines.isEmpty() ? Map.of() + : orderMapper.findOrderWeights(plan.getCustomerId(), lines.stream().map(InboundPlanLine::getOrderId) + .filter(Objects::nonNull).distinct().toList()).stream() + .collect(Collectors.toMap(InboundPlanOrderMapper.OrderWeightSnapshot::orderId, value -> value)); + BigDecimal totalGrossWeightKg = weights.values().stream() + .map(InboundPlanOrderMapper.OrderWeightSnapshot::grossWeightKg).filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal totalNetWeightKg = weights.values().stream() + .map(InboundPlanOrderMapper.OrderWeightSnapshot::netWeightKg).filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); List pallets = palletMapper.selectList(new LambdaQueryWrapper() .eq(InboundPlanPallet::getPlanId, plan.getId()).orderByAsc(InboundPlanPallet::getSequenceNo)); List allocations = allocationMapper.selectList( @@ -1116,11 +1126,14 @@ public class InboundPlanService { sourceOrder == null ? null : sourceOrder.customerName(), sourceOrder == null ? null : sourceOrder.orderType(), plan.getContainerNo(), authoritativeWarehouse.warehouseCode(), authoritativeWarehouse.warehouseName(), - plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(), + plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), totalGrossWeightKg, + totalNetWeightKg, plan.getTotalVolumeCbm(), lines.stream().mapToInt(InboundPlanLine::getRequiredPackageCount).sum(), allocations.stream().mapToInt(InboundPlanPalletAllocation::getPickedPackageCount).sum(), lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(), line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(), + weights.containsKey(line.getOrderId()) ? weights.get(line.getOrderId()).grossWeightKg() : BigDecimal.ZERO, + weights.containsKey(line.getOrderId()) ? weights.get(line.getOrderId()).netWeightKg() : BigDecimal.ZERO, line.getVolumeCbm(), line.getRequiredPackageCount(), line.getPlannedPalletCount())).toList(), pallets.stream().map(pallet -> new InboundPlanResponse.Pallet(pallet.getId(), pallet.getPalletNo(), pallet.getDestinationWarehouseId(), pallet.getDestinationWarehouseCode(), pallet.getDestinationWarehouseName(), 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 0e9bcf6c7..1d018878d 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 @@ -12,6 +12,7 @@ import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; +import java.math.BigDecimal; @Mapper public interface LastlegOrderMapper extends BaseMapper { @@ -27,6 +28,17 @@ public interface LastlegOrderMapper extends BaseMapper { record DispatchStatusRow(Long orderId, String dispatchStatus) { } + record OrderWeightRow(Long orderId, BigDecimal grossWeightKg, BigDecimal netWeightKg) { } + + @Select("") + List findOrderWeights(@Param("orderIds") List orderIds); + String CONTAINER_PROGRESS_FILTER = " FROM ll_order o LEFT JOIN ll_inbound_plan p " + "ON p.customer_id=o.customer_id AND p.source_order_id=o.id " + "WHERE o.source_container_booking_id IS NOT NULL AND o.source_container_booking_id<>'' " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java index 5ee77cd7b..49812a1c3 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java @@ -48,6 +48,7 @@ import java.nio.charset.StandardCharsets; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.security.MessageDigest; import java.util.ArrayList; @@ -94,9 +95,11 @@ public class LastlegOrderService { Map> mergeRelations = palletMergeRelations(page.getRecords()); Map containerNodes = containerNodes(page.getRecords()); Map dispatchStatuses = dispatchStatuses(page.getRecords()); + Map weights = orderWeights(page.getRecords()); return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()), StringUtils.hasText(order.getSourceContainerBookingId()) - ? containerNodes.get(order.getSourceContainerBookingId()) : null, dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"))).toList(), + ? containerNodes.get(order.getSourceContainerBookingId()) : null, + dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"), weights.get(order.getId()))).toList(), page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); } @@ -349,7 +352,8 @@ public class LastlegOrderService { value.getNodeCode(), value.getNodeAt(), value.getSource())) .sorted(java.util.Comparator.comparingInt(value -> MANUAL_NODE_CODES.indexOf(value.nodeCode()))).toList(); return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId()), - containerNode(order), dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED")), manualNodes, cargo, history.stream() + containerNode(order), dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED"), + orderWeights(List.of(order)).get(order.getId())), manualNodes, cargo, history.stream() .map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(), value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream() .map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(), @@ -625,8 +629,17 @@ public class LastlegOrderService { LastlegOrderMapper.DispatchStatusRow::dispatchStatus)); } + private Map orderWeights(List orders) { + if (orders.isEmpty()) return Map.of(); + List rows = orderMapper.findOrderWeights( + orders.stream().map(LastlegOrder::getId).toList()); + if (rows == null) return Map.of(); + return rows.stream().collect(Collectors.toMap(LastlegOrderMapper.OrderWeightRow::orderId, row -> row)); + } + private LastlegOrderSummary summary(LastlegOrder o, List relations, - String containerNode, String dispatchStatus) { + String containerNode, String dispatchStatus, + LastlegOrderMapper.OrderWeightRow weight) { List related = relations == null ? List.of() : relations; return new LastlegOrderSummary(o.getId(), displayOrderNo(o), o.getOrderType(), LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(), @@ -637,7 +650,8 @@ public class LastlegOrderService { o.getAbnormal(), o.getDetained(), o.getHoldChargeable(), o.getHoldReason(), Boolean.TRUE.equals(o.getManualNodeOverride()), Boolean.TRUE.equals(o.getManualNodeOverride()) ? "MANUAL" : "CONTAINER", o.getVersion() == null ? 0 : o.getVersion(), o.getPalletCount(), o.getPackageCount(), - o.getReceivedPackageCount(), packageDifference(o), o.getVolumeCbm(), + o.getReceivedPackageCount(), packageDifference(o), weight == null ? BigDecimal.ZERO : weight.grossWeightKg(), + weight == null ? BigDecimal.ZERO : weight.netWeightKg(), o.getVolumeCbm(), o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(), o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), dispatchStatus, o.getRelabelOrderNo(), relabelBusinessStatus(o.getRelabelStatus()), o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null), diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 3d20c42e9..81285ffcb 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -1389,6 +1389,8 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_item ( contact_name VARCHAR(128) NULL COMMENT '私人仓联系人快照', contact_phone VARCHAR(64) NULL COMMENT '私人仓联系电话快照', package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', + gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '毛重(千克)', + volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', source_stage VARCHAR(32) NOT NULL COMMENT '数据来源阶段', carton_marks VARCHAR(4000) NULL COMMENT '换标箱唛映射快照', @@ -1405,6 +1407,7 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_item ( SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='carton_marks'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN carton_marks VARCHAR(4000) NULL COMMENT ''换标箱唛映射快照'' AFTER source_stage')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='source_business_type'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN source_business_type VARCHAR(32) NULL COMMENT ''来源业务类型'' AFTER carton_marks')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='source_business_no'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN source_business_no VARCHAR(128) NULL COMMENT ''来源业务单号'' AFTER source_business_type')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='gross_weight_kg'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''毛重(千克)'' AFTER package_count, ADD COLUMN volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''材积重(千克)'' AFTER gross_weight_kg')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA或PRIVATE'' AFTER fba_warehouse_code, ADD COLUMN delivery_address VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照'' AFTER destination_type, ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'' AFTER delivery_address, ADD COLUMN contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人快照'' AFTER postal_code, ADD COLUMN contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话快照'' AFTER contact_name')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock ( @@ -1507,6 +1510,8 @@ CREATE TABLE IF NOT EXISTS ll_outbound_order ( outbound_no VARCHAR(64) NOT NULL COMMENT '出库单号', dispatch_order_id BIGINT NULL COMMENT '派车单ID,一件代发为空', dispatch_no VARCHAR(64) NULL COMMENT '派车单号,一件代发为空', overseas_warehouse_id VARCHAR(128) NULL COMMENT '海外仓ID', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_OUTBOUND' COMMENT '业务状态', package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', pallet_count INT NOT NULL DEFAULT 0 COMMENT '托盘数', + gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '毛重(千克)', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', + volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键', created_by VARCHAR(128) NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', outbound_type VARCHAR(32) NOT NULL DEFAULT 'DISPATCH_DELIVERY' COMMENT '出库类型:派送转出库、一件代发', lx_codes VARCHAR(1000) NULL COMMENT '关联一件代发领星入库单代码汇总', @@ -1521,7 +1526,7 @@ CREATE TABLE IF NOT EXISTS ll_outbound_pallet_item ( id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', outbound_order_id BIGINT NOT NULL COMMENT '出库单ID', warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID', pallet_no VARCHAR(128) NULL COMMENT '托盘号', destination_warehouse_id VARCHAR(128) NULL COMMENT '目的仓ID', destination_warehouse_code VARCHAR(128) NULL COMMENT '目的仓编码快照', destination_warehouse_name VARCHAR(255) NULL COMMENT '目的仓名称快照', - package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', picked_package_count INT NOT NULL DEFAULT 0 COMMENT '已拣箱数', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code VARCHAR(128) NULL COMMENT '一件代发SKU', order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照', + package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '毛重(千克)', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', picked_package_count INT NOT NULL DEFAULT 0 COMMENT '已拣箱数', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code VARCHAR(128) NULL COMMENT '一件代发SKU', order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,sku_code), KEY idx_ll_outbound_pallet_list (customer_id,outbound_order_id), KEY idx_ll_outbound_pallet_destination (customer_id,destination_warehouse_code) @@ -1562,6 +1567,8 @@ CREATE TABLE IF NOT EXISTS ll_outbound_pda_pick ( SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status')); PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='gross_weight_kg'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''毛重(千克)'' AFTER pallet_count, ADD COLUMN volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''材积重(千克)'' AFTER gross_weight_kg, ADD COLUMN volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''体积(立方米)'' AFTER volume_weight_kg')); PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='gross_weight_kg'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''毛重(千克)'' AFTER package_count, ADD COLUMN volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''材积重(千克)'' AFTER gross_weight_kg, ADD COLUMN volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''体积(立方米)'' AFTER volume_weight_kg')); PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='lx_codes'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN lx_codes VARCHAR(1000) NULL COMMENT ''关联一件代发领星入库单代码汇总'' AFTER outbound_type')); PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'), 'SELECT 1', 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''一件代发SKU''')); 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 791bf1a1d..94b3a393c 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -185,7 +185,7 @@ paths: /api/platform/v1/last-leg/orders: get: summary: 分页查询尾程订单 - description: 权限 platform:overseas-warehouse:last-mile-order:list;平台管理员省略 customerId 时查询全部客户。containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁。 + description: 权限 platform:overseas-warehouse:last-mile-order:list;平台管理员省略 customerId 时查询全部客户。containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。 security: [{ bearerAuth: [] }] parameters: - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } @@ -206,7 +206,7 @@ paths: /api/platform/v1/last-leg/orders/{id}: get: summary: 查询尾程订单详情 - description: 权限 platform:overseas-warehouse:last-mile-order:list;返回订单、货物、状态历史、费用明细和操作记录。订单中的 containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁。 + description: 权限 platform:overseas-warehouse:last-mile-order:list;返回订单、货物、状态历史、费用明细和操作记录。订单中的 containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。 security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: integer, format: int64 } } @@ -463,7 +463,7 @@ paths: /api/platform/v1/last-leg/inbound-plans: get: summary: 平台分页查询海外入库订单 - description: 权限 platform:overseas-warehouse:inbound-order:list;列表中的 overseasWarehouseCode/overseasWarehouseName 按入库计划冻结接柜仓ID从海外仓权威目录解析,warehouseCode/warehouseName 仍表示目的仓。 + description: 权限 platform:overseas-warehouse:inbound-order:list;overseasWarehouseCode/overseasWarehouseName 只取尾程订单预分配接柜仓事实,并按入库计划冻结接柜仓ID从海外仓权威目录解析;warehouseCode/warehouseName 仍表示目的仓。 security: [{ bearerAuth: [] }] parameters: - { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } } @@ -2272,6 +2272,8 @@ components: destinationWarehouseName: { type: string } fbaWarehouseCode: { type: string } packageCount: { type: integer } + grossWeightKg: { type: number, description: 排车托盘冻结毛重(千克) } + volumeWeightKg: { type: number, description: 排车托盘冻结材积重(千克) } volumeCbm: { type: number } sourceStage: { type: string, enum: [PLAN, RECEIVED] } cartonMarks: { type: string, nullable: true, description: 该托盘箱号,逗号分隔 } @@ -3006,7 +3008,7 @@ components: properties: { records: { type: array, items: { $ref: '#/components/schemas/InboundPlanSummary' } } } InboundPlanSummary: type: object - required: [lineId, planId, planNo, customerId, orderId, inboundCode, overseasWarehouseCode, overseasWarehouseName, packageCount, pickedPackageCount, palletCount, assigned, mergeAllowed, urgent, status, createdAt, version] + required: [lineId, planId, planNo, customerId, orderId, inboundCode, overseasWarehouseCode, overseasWarehouseName, packageCount, pickedPackageCount, grossWeightKg, netWeightKg, palletCount, assigned, mergeAllowed, urgent, status, createdAt, version] properties: lineId: { type: string, pattern: '^[0-9]+$' } planId: { type: string, pattern: '^[0-9]+$' } @@ -3025,6 +3027,8 @@ components: customerName: { type: string, nullable: true } packageCount: { type: integer } pickedPackageCount: { type: integer } + grossWeightKg: { type: number, description: 头程冻结货物毛重(千克) } + netWeightKg: { type: number, description: 头程冻结货物净重(千克) } volumeCbm: { type: number, nullable: true } palletCount: { type: integer } assigned: { type: boolean, description: 入库计划是否已分配 } @@ -3040,7 +3044,7 @@ components: version: { type: integer } InboundPlanDetail: type: object - required: [id, planNo, customerId, overseasWarehouseCode, overseasWarehouseName, status, lineCount, palletCount, totalVolumeCbm, expectedPackageCount, pickedPackageCount, lines, pallets, allocations, version, createdAt, updatedAt] + required: [id, planNo, customerId, overseasWarehouseCode, overseasWarehouseName, status, lineCount, palletCount, totalGrossWeightKg, totalNetWeightKg, totalVolumeCbm, expectedPackageCount, pickedPackageCount, lines, pallets, allocations, version, createdAt, updatedAt] properties: id: { type: string, pattern: '^[0-9]+$' } planNo: { type: string } @@ -3055,6 +3059,8 @@ components: status: { type: string } lineCount: { type: integer } palletCount: { type: integer } + totalGrossWeightKg: { type: number, description: 入库单关联头程货物毛重合计(千克) } + totalNetWeightKg: { type: number, description: 入库单关联头程货物净重合计(千克) } totalVolumeCbm: { type: number } expectedPackageCount: { type: integer } pickedPackageCount: { type: integer } @@ -3148,6 +3154,8 @@ components: destinationWarehouseId: { type: string, nullable: true } destinationWarehouseCode: { type: string, nullable: true } destinationWarehouseName: { type: string, nullable: true } + grossWeightKg: { type: number, description: 本入仓码头程冻结货物毛重(千克) } + netWeightKg: { type: number, description: 本入仓码头程冻结货物净重(千克) } volumeCbm: { type: number } requiredPackageCount: { type: integer } plannedPalletCount: { type: integer } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java index 64226bf1e..586c284ec 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java @@ -142,6 +142,8 @@ class DeliveryTransferServiceTest { assertEquals("WAIT_OUTBOUND", outbound.getValue().getStatus()); assertEquals(2, outbound.getValue().getPalletCount()); assertEquals(20, outbound.getValue().getPackageCount()); + assertEquals(new BigDecimal("20"), outbound.getValue().getGrossWeightKg()); + assertEquals(new BigDecimal("2.0"), outbound.getValue().getVolumeCbm()); assertEquals("transfer-101", outbound.getValue().getIdempotencyKey()); assertTrue(outbound.getValue().getOutboundNo().matches("OB-WH-\\d{8}-\\d{6}")); assertTrue(delivery.getAllValues().stream().allMatch(value -> outbound.getValue().getId().equals(value.getOutboundOrderId()))); @@ -152,6 +154,10 @@ class DeliveryTransferServiceTest { .map(OutboundPalletItem::getDestinationWarehouseCode).toList()); assertEquals(List.of("IN-A", "IN-B"), pallets.getAllValues().stream() .map(OutboundPalletItem::getInboundCode).toList()); + assertEquals(List.of(new BigDecimal("12"), new BigDecimal("8")), pallets.getAllValues().stream() + .map(OutboundPalletItem::getGrossWeightKg).toList()); + assertEquals(List.of(new BigDecimal("12"), new BigDecimal("8")), delivery.getAllValues().stream() + .map(DeliveryOrder::getTotalWeightKg).toList()); assertEquals(20, result.packageCount()); assertEquals(2, result.palletCount()); assertEquals(2, result.deliveries().size()); @@ -929,6 +935,9 @@ class DeliveryTransferServiceTest { value.setDestinationWarehouseId("DEST-" + destinationCode); value.setDestinationWarehouseCode(destinationCode); value.setDestinationWarehouseName(destinationName); value.setInboundCode(inboundCode); value.setOrderRefs("LMO-1"); value.setPackageCount(packages); + value.setGrossWeightKg(BigDecimal.valueOf(packages)); + value.setVolumeWeightKg(BigDecimal.valueOf(packages * 2L)); + value.setVolumeCbm(BigDecimal.valueOf(packages, 1)); value.setSourceStage("RECEIVED"); return value; } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java index d7f2e7558..fc143f7b2 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java @@ -434,7 +434,7 @@ class ContainerInboundPlanConversionCoordinatorTest { private InboundPlanResponse plan(String planNo, List pallets) { return new InboundPlanResponse(21L, planNo, "100", 11L, "FL-WH-1", "验收客户", "ONE_CLICK_DROPSHIP", "MSCU6639870", "US-LAX-01", - "Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.ONE, 10, 0, + "Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.TEN, new BigDecimal("8"), BigDecimal.ONE, 10, 0, List.of(), pallets, List.of(), 1, LocalDate.of(2026, 9, 10), "WOOD", false, false, false, "MANUAL", "自动生成", null, null); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java index 31be95377..3db84f19c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java @@ -52,7 +52,8 @@ class InboundPlanPaginationTest { LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59)); InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100", 3L, "IN-1", null, "FIRST_LEG", 193000000000200003L, "ACY2", "错误目的仓", "W1", "ONT", "安大略仓", - "ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, true, true, true, "PENDING_ASSIGNMENT", null, null, null, + "ONT8", "客户A", 10, 0, new BigDecimal("30"), new BigDecimal("22"), BigDecimal.ONE, + 1, true, true, true, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0); when(queryMapper.count("100", query)).thenReturn(11L); when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row)); @@ -87,6 +88,7 @@ class InboundPlanPaginationTest { "p.overseas_warehouse_id=#{q.overseasWarehouseId}"); assertThat(sql).contains("p.assigned_flag assigned", "p.merge_required mergeAllowed", "p.urgent_flag urgent"); + assertThat(sql).contains("grossWeightKg", "netWeightKg", "ll_order_detail"); assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}"); assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId", "q.destinationWarehouseCode", "q.warehouse", diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java index 347c291a7..3ff5b751c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java @@ -94,10 +94,13 @@ class LastlegOrderServiceTest { order.setSourceContainerBookingId("BOOKING-1"); LastlegOrderSummary summary = ReflectionTestUtils.invokeMethod( - service, "summary", order, List.of(), null, "UNSCHEDULED"); + service, "summary", order, List.of(), null, "UNSCHEDULED", + new LastlegOrderMapper.OrderWeightRow(11L, new java.math.BigDecimal("30"), new java.math.BigDecimal("22"))); assertNotNull(summary); assertEquals("BOOKING-1", summary.sourceContainerBookingId()); + assertEquals(new java.math.BigDecimal("30"), summary.grossWeightKg()); + assertEquals(new java.math.BigDecimal("22"), summary.netWeightKg()); } @BeforeEach diff --git a/order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java b/order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java index 7e72dd35a..739505aa2 100644 --- a/order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java +++ b/order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java @@ -36,6 +36,10 @@ public interface FirstlegInboundClient { ApiResponse updateDeclaration(@RequestHeader("X-Internal-Service-Token") String token, @RequestBody OrderDeclarationDtos.UpdatedEvent event); + @PostMapping("/order-remarks-updated") + ApiResponse updateRemarks(@RequestHeader("X-Internal-Service-Token") String token, + @RequestBody OrderRemarksSyncCommand command); + @PostMapping("/order-channel-changes/validate") ApiResponse validateOrderChannelChange( @RequestHeader("X-Internal-Service-Token") String token, @@ -51,6 +55,9 @@ public interface FirstlegInboundClient { String channelCode) {} record OrderChannelChangeValidationResponse(long orderId, long orderVersion, String channelCode, String mainStatus) {} + record OrderRemarksSyncCommand(long tenantId, long orderId, String customerRemark, String internalRemark, + String warehouseRemark, String customerServiceRemark, + long operatorId, String operatorName) {} record OrderApprovedInboundCommand( long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName, diff --git a/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java b/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java index fdddfec73..21f720bc3 100644 --- a/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java +++ b/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java @@ -16,6 +16,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -32,6 +34,7 @@ import java.util.stream.Collectors; @RequestMapping("/internal/order/v1/cargoes") public class InternalOrderCargoController { @Resource private OrderRepository orders; + @Resource private OrderApplicationService orderApplicationService; @Resource private PrintableInboundSnapshotService printableInboundSnapshots; @Value("${baiyue.security.internal-service-token}") private String internalToken; private byte[] expectedToken; @@ -146,6 +149,21 @@ public class InternalOrderCargoController { .collect(Collectors.joining("、")))); } + @PutMapping("/{tenantId}/{orderId}/remarks") + public ApiResponse updateRemarks( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @PathVariable long tenantId, @PathVariable long orderId, + @RequestBody InternalRemarksUpdateRequest request) { + requireToken(token); + if (tenantId <= 0 || orderId <= 0 || request == null || request.operatorId() <= 0) { + throw new BizException(ResultCode.BAD_REQUEST, "客户、订单和操作人必须大于0"); + } + var result = orderApplicationService.updateRemarksFromFirstleg(tenantId, request.operatorId(), orderId, + request.internalRemark(), request.warehouseRemark(), request.customerServiceRemark()); + return ApiResponse.success(new InternalRemarksUpdateResponse(result.version(), result.customerRemark(), + result.internalRemark(), result.warehouseRemark(), result.customerServiceRemark())); + } + private BigDecimal sum(List cargoes, java.util.function.Function getter) { List values = cargoes.stream().map(getter).filter(Objects::nonNull).toList(); @@ -205,4 +223,10 @@ public class InternalOrderCargoController { public record MeasurementDeclarationSnapshot( String referenceId, BigDecimal originalGrossWeightKg, BigDecimal originalVolumeCbm, String defaultActualGoodsName) {} + + public record InternalRemarksUpdateRequest(String internalRemark, String warehouseRemark, + String customerServiceRemark, long operatorId) {} + public record InternalRemarksUpdateResponse(int version, String customerRemark, String internalRemark, + String warehouseRemark, String customerServiceRemark) {} + } diff --git a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java index 7c18df6c5..044907daa 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java @@ -122,6 +122,7 @@ public class PlatformUnifiedOrderController { @PathVariable long orderId, @Valid @RequestBody OrderRequests.RemarksUpdateRequest request) { long scopedTenant = OrderScopeGuard.platform(user, tenantId); - return ApiResponse.success(orderService.updateRemarks(scopedTenant, user.userId(), orderId, request)); + return ApiResponse.success(orderService.updateRemarks( + scopedTenant, user.userId(), user.username(), orderId, request)); } } diff --git a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java index 6620f5df0..3b349c2f7 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -271,6 +271,12 @@ public class OrderApplicationService { @Transactional public OrderRequests.RemarksUpdateResponse updateRemarks(long tenantId, long userId, long id, OrderRequests.RemarksUpdateRequest request) { + return updateRemarks(tenantId, userId, null, id, request); + } + + @Transactional + public OrderRequests.RemarksUpdateResponse updateRemarks(long tenantId, long userId, String operatorName, long id, + OrderRequests.RemarksUpdateRequest request) { OrderEntity order = require(tenantId, id); String internalRemark = normalizeRemark(request.internalRemark()); String warehouseRemark = normalizeRemark(request.warehouseRemark()); @@ -284,10 +290,57 @@ public class OrderApplicationService { note.setTenantId(tenantId); note.setOrderId(id); note.setContent(internalRemark); note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note); } + syncRemarksToFirstleg(tenantId, id, order.getCustomerRemark(), internalRemark, warehouseRemark, customerServiceRemark, + userId, operatorName); return new OrderRequests.RemarksUpdateResponse(order.getVersion() + 1, order.getCustomerRemark(), internalRemark, warehouseRemark, customerServiceRemark); } + @Transactional + public OrderRequests.RemarksUpdateResponse updateRemarksFromFirstleg(long tenantId, long userId, long id, + String internalValue, + String warehouseValue, + String customerServiceValue) { + requireRemarkLength(internalValue); + requireRemarkLength(warehouseValue); + requireRemarkLength(customerServiceValue); + OrderEntity order = require(tenantId, id); + String internalRemark = normalizeRemark(internalValue); + String warehouseRemark = normalizeRemark(warehouseValue); + String customerServiceRemark = normalizeRemark(customerServiceValue); + if (repository.updateRemarks(tenantId, id, order.getVersion(), userId, + internalRemark, warehouseRemark, customerServiceRemark) != 1) { + throw new BizException(ResultCode.CONFLICT, "订单备注已被其他人修改,请刷新后重试"); + } + if (!Objects.equals(order.getInternalRemark(), internalRemark) && internalRemark != null) { + OrderInternalNoteEntity note = new OrderInternalNoteEntity(); + note.setTenantId(tenantId); note.setOrderId(id); note.setContent(internalRemark); + note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note); + } + return new OrderRequests.RemarksUpdateResponse(order.getVersion() + 1, order.getCustomerRemark(), + internalRemark, warehouseRemark, customerServiceRemark); + } + + private void syncRemarksToFirstleg(long tenantId, long orderId, String customerRemark, String internalRemark, + String warehouseRemark, String customerServiceRemark, + long operatorId, String operatorName) { + FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable(); + if (client == null) return; + ApiResponse response = client.updateRemarks(internalToken, + new FirstlegInboundClient.OrderRemarksSyncCommand(tenantId, orderId, customerRemark, internalRemark, + warehouseRemark, customerServiceRemark, operatorId, operatorName)); + if (response == null || !response.successful()) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, + response == null ? "头程备注同步暂不可用" : response.message()); + } + } + + private static void requireRemarkLength(String value) { + if (value != null && value.length() > 1000) { + throw new BizException(ResultCode.BAD_REQUEST, "每项备注不能超过1000个字符"); + } + } + private static String normalizeRemark(String value) { if (value == null || value.trim().isEmpty()) return null; return value.trim(); diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index eb5873cb7..3270cf299 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -59,6 +59,34 @@ paths: application/json: schema: {$ref: '#/components/schemas/CargoLabelBindingListEnvelope'} '403': {description: 内部服务令牌无效} + put: + security: [] + summary: Firstleg 维护订单权威协作备注 + description: >- + 仅接受内部服务令牌。预装清单编辑备注时写回 Order 权威订单记录; + 客户备注保持只读,内部、仓库和客服备注统一按订单版本乐观更新。 + parameters: + - name: X-Internal-Service-Token + in: header + required: true + schema: {type: string} + - {name: tenantId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [operatorId] + properties: + internalRemark: {type: string, nullable: true, maxLength: 1000} + warehouseRemark: {type: string, nullable: true, maxLength: 1000} + customerServiceRemark: {type: string, nullable: true, maxLength: 1000} + operatorId: {type: integer, format: int64, minimum: 1} + responses: + '200': {description: 返回订单最新版本和四类备注} + '403': {description: 内部服务令牌无效} /internal/order/v1/cargoes/{tenantId}/{orderId}/printable-inbound-snapshot: post: security: [] diff --git a/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java b/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java index 99608e18e..e144c7c38 100644 --- a/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java +++ b/order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java @@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.PostMapping; @TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token") class InternalOrderCargoControllerTest { @MockBean private OrderRepository orders; + @MockBean private OrderApplicationService orderApplicationService; @MockBean private PrintableInboundSnapshotService printableInboundSnapshots; @Resource private InternalOrderCargoController controller; diff --git a/order/src/test/java/com/baiyue/order/service/OrderRemarksSynchronizationTest.java b/order/src/test/java/com/baiyue/order/service/OrderRemarksSynchronizationTest.java new file mode 100644 index 000000000..7c1eb5513 --- /dev/null +++ b/order/src/test/java/com/baiyue/order/service/OrderRemarksSynchronizationTest.java @@ -0,0 +1,53 @@ +package com.baiyue.order.service; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.order.client.FirstlegInboundClient; +import com.baiyue.order.dto.OrderRequests; +import com.baiyue.order.entity.OrderEntity; +import com.baiyue.order.repository.OrderRepository; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class OrderRemarksSynchronizationTest { + @Test + void platformUpdateSynchronizesAuthoritativeRemarksToFirstleg() { + OrderRepository repository = mock(OrderRepository.class); + FirstlegInboundClient firstleg = mock(FirstlegInboundClient.class); + @SuppressWarnings("unchecked") + ObjectProvider provider = mock(ObjectProvider.class); + OrderApplicationService service = new OrderApplicationService(); + ReflectionTestUtils.setField(service, "repository", repository); + ReflectionTestUtils.setField(service, "firstlegProvider", provider); + ReflectionTestUtils.setField(service, "internalToken", "internal-token"); + + OrderEntity order = new OrderEntity(); + order.setId(20L); + order.setTenantId(10L); + order.setVersion(7); + order.setCustomerRemark("客户备注"); + when(repository.findOwned(10L, 20L)).thenReturn(order); + when(repository.updateRemarks(10L, 20L, 7, 30L, "内部", "仓库", "客服")).thenReturn(1); + when(provider.getIfAvailable()).thenReturn(firstleg); + when(firstleg.updateRemarks(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(2)); + + var response = service.updateRemarks(10L, 30L, "操作人", 20L, + new OrderRequests.RemarksUpdateRequest(7, " 内部 ", "仓库", "客服")); + + assertThat(response.version()).isEqualTo(8); + ArgumentCaptor command = + ArgumentCaptor.forClass(FirstlegInboundClient.OrderRemarksSyncCommand.class); + verify(firstleg).updateRemarks(org.mockito.ArgumentMatchers.eq("internal-token"), command.capture()); + assertThat(command.getValue().customerRemark()).isEqualTo("客户备注"); + assertThat(command.getValue().internalRemark()).isEqualTo("内部"); + assertThat(command.getValue().warehouseRemark()).isEqualTo("仓库"); + assertThat(command.getValue().customerServiceRemark()).isEqualTo("客服"); + } +}