fix: complete order measurement downstream flow

This commit is contained in:
2026-09-18 14:58:40 +08:00
parent 02d2474141
commit c8a99f9fff
43 changed files with 621 additions and 52 deletions
@@ -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',
+1 -1
View File
@@ -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 的当前唯一版本 |
@@ -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<RemarksUpdateResponse> 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,
@@ -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<Integer> 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<PageResult<UnifiedInboundSummary>> unifiedPage(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@@ -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
) {
}
}
@@ -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<LoadingPlanItemEntity> {
@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&lt;&gt;#{excludedItemId})
AND NOT (i.customer_remark <=> #{customerRemark}
AND i.internal_remark <=> #{internalRemark}
AND i.warehouse_remark <=> #{warehouseRemark}
AND i.customer_service_remark <=> #{customerServiceRemark})
""")
List<Long> 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&lt;&gt;#{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<Long> 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);
}
@@ -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<LoadingPlanEntity> {
@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("""
<script>
UPDATE fl_loading_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE plan_status IN ('EDITING','LOADING') AND id IN
<foreach collection="planIds" item="planId" open="(" separator="," close=")">#{planId}</foreach>
</script>
""")
int touchForOrderRemarks(@Param("planIds") List<Long> planIds,
@Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
}
@@ -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) {
@@ -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<Long> 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<Long> 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<Long> 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;
}
}
@@ -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<OrderCargoClient.RemarksUpdateResponse> 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,
@@ -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 内部)
@@ -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;
@@ -35,6 +35,7 @@ class OrderDeclarationSyncServiceTest {
}
@MockBean private OrderDeclarationSyncMapper mapper;
@MockBean private OrderRemarksSyncService remarksSync;
@Resource private OrderDeclarationSyncService service;
@Resource private ObjectMapper json;
@@ -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));
}
}
@@ -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;
@@ -16,7 +16,9 @@ public final class DeliveryResponses {
List<TransferDelivery> 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<DeliveryRow> deliveries, List<OutboundPalletRow> pallets) {}
public record OneClickCandidate(Long inventoryPositionId, String orderNo, String customerId, String customerName, String overseasWarehouseId,
String overseasWarehouseName, String palletNo, String inboundCode,
@@ -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;
@@ -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;
@@ -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<DispatchItem> items) {
List<Long> palletIds = items.stream().map(DispatchItem::getWarehousePalletId)
.filter(Objects::nonNull).distinct().toList();
List<WarehouseTransferPallet> transferPallets = palletIds.isEmpty() ? List.of()
: warehouseTransferPallets.selectList(new LambdaQueryWrapper<WarehouseTransferPallet>()
.eq(WarehouseTransferPallet::getCustomerId, customerId)
.in(WarehouseTransferPallet::getWarehousePalletId, palletIds));
Set<Long> 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<Long> 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<LastlegOrderDetail>()
.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<DispatchItem> items,
java.util.function.Function<DispatchItem, BigDecimal> 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) {
@@ -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) {
}
@@ -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;
@@ -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);
}
@@ -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,
@@ -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);
}
@@ -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,
@@ -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("<script>SELECT o.id AS orderId," +
"COALESCE(SUM(d.gross_weight_kg),MAX(i.gross_weight_kg),0) AS grossWeightKg," +
"COALESCE(SUM(d.net_weight_kg),MAX(i.net_weight_kg),0) AS netWeightKg " +
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id AND d.customer_id=o.customer_id " +
"LEFT JOIN ll_inbound_order i ON i.id=o.id AND i.customer_id=o.customer_id " +
"WHERE o.customer_id=#{customerId} AND o.id IN " +
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"GROUP BY o.id</script>")
List<OrderWeightSnapshot> findOrderWeights(@Param("customerId") String customerId,
@Param("orderIds") List<Long> 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}")
@@ -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," +
@@ -1077,6 +1077,16 @@ public class InboundPlanService {
}
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
.eq(InboundPlanLine::getPlanId, plan.getId()).orderByAsc(InboundPlanLine::getId));
Map<Long, InboundPlanOrderMapper.OrderWeightSnapshot> 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<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
.eq(InboundPlanPallet::getPlanId, plan.getId()).orderByAsc(InboundPlanPallet::getSequenceNo));
List<InboundPlanPalletAllocation> 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(),
@@ -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<LastlegOrder> {
@@ -27,6 +28,17 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
record DispatchStatusRow(Long orderId, String dispatchStatus) { }
record OrderWeightRow(Long orderId, BigDecimal grossWeightKg, BigDecimal netWeightKg) { }
@Select("<script>SELECT o.id AS orderId," +
"COALESCE(SUM(d.gross_weight_kg),MAX(i.gross_weight_kg),0) AS grossWeightKg," +
"COALESCE(SUM(d.net_weight_kg),MAX(i.net_weight_kg),0) AS netWeightKg " +
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id AND d.customer_id=o.customer_id " +
"LEFT JOIN ll_inbound_order i ON i.id=o.id AND i.customer_id=o.customer_id WHERE o.id IN " +
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"GROUP BY o.id</script>")
List<OrderWeightRow> findOrderWeights(@Param("orderIds") List<Long> 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&lt;&gt;'' " +
@@ -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<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(page.getRecords());
Map<String, String> containerNodes = containerNodes(page.getRecords());
Map<Long, String> dispatchStatuses = dispatchStatuses(page.getRecords());
Map<Long, LastlegOrderMapper.OrderWeightRow> 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<Long, LastlegOrderMapper.OrderWeightRow> orderWeights(List<LastlegOrder> orders) {
if (orders.isEmpty()) return Map.of();
List<LastlegOrderMapper.OrderWeightRow> 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<LastlegOrderMapper.PalletMergeRelationRow> relations,
String containerNode, String dispatchStatus) {
String containerNode, String dispatchStatus,
LastlegOrderMapper.OrderWeightRow weight) {
List<LastlegOrderMapper.PalletMergeRelationRow> 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),
@@ -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'''));
@@ -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 }
@@ -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;
}
@@ -434,7 +434,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
private InboundPlanResponse plan(String planNo, List<InboundPlanResponse.Pallet> 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);
}
@@ -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",
@@ -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
@@ -36,6 +36,10 @@ public interface FirstlegInboundClient {
ApiResponse<Void> updateDeclaration(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody OrderDeclarationDtos.UpdatedEvent event);
@PostMapping("/order-remarks-updated")
ApiResponse<Integer> updateRemarks(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody OrderRemarksSyncCommand command);
@PostMapping("/order-channel-changes/validate")
ApiResponse<OrderChannelChangeValidationResponse> 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,
@@ -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<InternalRemarksUpdateResponse> 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<OrderCargoEntity> cargoes,
java.util.function.Function<OrderCargoEntity, BigDecimal> getter) {
List<BigDecimal> 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) {}
}
@@ -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));
}
}
@@ -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<Integer> 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();
@@ -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: []
@@ -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;
@@ -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<FirstlegInboundClient> 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<FirstlegInboundClient.OrderRemarksSyncCommand> 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("客服");
}
}