Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # deploy/maven/settings-cn.xml
This commit is contained in:
@@ -2,38 +2,4 @@
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>aliyun-public</id>
|
||||
<name>阿里云 Maven 公共镜像</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<mirrorOf>central</mirrorOf>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>国内公共镜像</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<url>https://repo.maven.apache.org/maven2</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>false</enabled></snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>central</id>
|
||||
<url>https://repo.maven.apache.org/maven2</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>false</enabled></snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<activeProfiles>
|
||||
<activeProfile>国内公共镜像</activeProfile>
|
||||
</activeProfiles>
|
||||
</settings>
|
||||
|
||||
@@ -8060,7 +8060,7 @@ CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
@@ -13269,7 +13269,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'bo
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'created_at', '创建时间');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'created_by', '创建人ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'deleted', '逻辑删除');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'edge_addition_cm', '每个尺寸方向的单侧加宽厘米数');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'edge_addition_cm', '长宽高各自增加一次的厘米数');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'default_calculation_mode', '订单外箱规默认计算方式:ADD或MULTIPLY');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'id', '配置ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'initialization_key', '创建流程幂等键及补偿归属');
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.PageResult; import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*; import com.baiyue.firstleg.service.FirstlegScopeGuard; import com.baiyue.firstleg.service.SmartLoadingService;
|
||||
import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.annotation.Resource; import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List;
|
||||
@@ -13,7 +13,9 @@ public class PlatformSmartLoadingController {
|
||||
@GetMapping("/candidates") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<List<Candidate>> candidates(@AuthenticationPrincipal AuthenticatedUser u,@RequestParam @Positive long bookingId,@RequestParam @Positive long warehouseId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.candidates(bookingId,warehouseId));}
|
||||
@GetMapping("/inbound-candidates") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<List<InboundCandidate>> inboundCandidates(@AuthenticationPrincipal AuthenticatedUser u,@RequestParam @Positive long bookingId,@RequestParam @Positive long warehouseId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.inboundCandidates(bookingId,warehouseId));}
|
||||
public ApiResponse<PageResult<InboundCandidate>> inboundCandidates(@AuthenticationPrincipal AuthenticatedUser u,@Valid InboundCandidateQuery q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.inboundCandidates(q));}
|
||||
@GetMapping("/containers") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<PageResult<WorkbenchContainer>> containers(@AuthenticationPrincipal AuthenticatedUser u,@Valid WorkbenchContainerQuery q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.containers(q));}
|
||||
@GetMapping("/plans/by-booking/{bookingId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<Plan> draftByBooking(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long bookingId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.draftByBooking(bookingId));}
|
||||
@GetMapping("/plans/{planId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List;
|
||||
public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
public record ChannelSummary(String channelCode, String channelName,
|
||||
long candidateInboundCount, long availablePackageCount, long availableContainerCount) {}
|
||||
public enum CandidateOrderStatus { PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING }
|
||||
public record InboundCandidateQuery(
|
||||
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
|
||||
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode,
|
||||
LocalDate expectedArrivalStart, LocalDate expectedArrivalEnd, CandidateOrderStatus orderStatus,
|
||||
@Min(1) long page, @Min(1) @Max(200) long size) {
|
||||
public InboundCandidateQuery {
|
||||
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
public record GroupMemberCandidate(Long accountId, Long inboundId, Long orderId, String orderNo,
|
||||
String inboundCode, String customerName, Integer availablePackageCount,
|
||||
String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
|
||||
public record InboundCandidate(Long accountId, Long inboundId, Long orderId, String orderNo, String inboundCode,
|
||||
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsSummary,
|
||||
Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg,
|
||||
BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm,
|
||||
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes,
|
||||
Boolean locked, String blockReason, String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
|
||||
Boolean locked, String blockReason, String orderStatusSnapshot, LocalDateTime expectedArrivalAt,
|
||||
List<GroupMemberCandidate> groupMembers) {
|
||||
public InboundCandidate { groupMembers = groupMembers == null ? List.of() : List.copyOf(groupMembers); }
|
||||
public Long customerId() { return accountId; }
|
||||
}
|
||||
public record WorkbenchContainerQuery(
|
||||
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
|
||||
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode,
|
||||
LocalDate expectedArrivalStart, LocalDate expectedArrivalEnd, CandidateOrderStatus orderStatus,
|
||||
@Min(1) long page, @Min(1) @Max(200) long size) {
|
||||
public WorkbenchContainerQuery {
|
||||
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
public record WorkbenchContainer(Long bookingId, String bookingNo, String cabinetNo, String containerNo,
|
||||
Long warehouseId, String warehouseName, String channelCode, String channelName, String containerType,
|
||||
Long planId, String planNo, String planStatus, Integer version) {}
|
||||
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
|
||||
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
|
||||
BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 客户固定箱规的内径和每侧加宽配置。 */
|
||||
/** 客户固定箱规的内径和长、宽、高尺寸增加值配置。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_customer_dimension_config")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.ibatis.annotations.*; import java.util.List;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import org.apache.ibatis.annotations.*; import java.util.List; import java.util.Set;
|
||||
@Mapper public interface SmartLoadingCandidateMapper {
|
||||
@Select("""
|
||||
SELECT b.channel_code channelCode,COUNT(DISTINCT b.id) availableContainerCount
|
||||
@@ -83,6 +83,95 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
""")
|
||||
List<Candidate> candidates(@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
|
||||
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
|
||||
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
|
||||
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
|
||||
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
|
||||
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
|
||||
-COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
|
||||
WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
|
||||
AND split_item.parent_inbound_id=o.id
|
||||
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0) availablePackageCount,
|
||||
COALESCE(o.actual_gross_weight_kg / NULLIF(o.total_package_count, 0), 0) unitGrossWeightKg,
|
||||
COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg,
|
||||
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm,
|
||||
NULL coLoadGroupId,NULL groupNo,NULL memberCount,NULL groupTotalBoxes,
|
||||
NULL locked,NULL blockReason,o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
AND m.status='APPROVED'
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN (
|
||||
SELECT tenant_id,measurement_id,inbound_detail_id,SUM(package_count) package_count,
|
||||
COALESCE(SUM(COALESCE(total_volume_cbm,COALESCE(unit_volume_cbm,0)*package_count))
|
||||
/ NULLIF(SUM(package_count),0),0) unit_volume_cbm
|
||||
FROM fl_inbound_measurement_item
|
||||
WHERE inbound_detail_id IS NOT NULL AND package_count>0
|
||||
GROUP BY tenant_id,measurement_id,inbound_detail_id
|
||||
) i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE (#{warehouseId} IS NULL OR o.warehouse_id=#{warehouseId})
|
||||
AND o.process_status='WAIT_SPLIT'
|
||||
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
|
||||
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
|
||||
+COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
|
||||
WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
|
||||
AND split_item.parent_inbound_id=o.id
|
||||
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0)
|
||||
ORDER BY o.inbounded_at ASC, o.id ASC, i.inbound_detail_id ASC
|
||||
""")
|
||||
List<Candidate> globalCandidates(@Param("warehouseId") Long warehouseId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT b.id bookingId,b.booking_no bookingNo,b.cabinet_no cabinetNo,b.container_no containerNo,
|
||||
b.warehouse_id warehouseId,b.warehouse_name warehouseName,b.channel_code channelCode,
|
||||
b.channel_name channelName,b.container_type containerType,p.id planId,p.plan_no planNo,
|
||||
CASE WHEN p.id IS NULL OR NOT EXISTS (SELECT 1 FROM fl_loading_plan_item state_item
|
||||
WHERE state_item.loading_plan_id=p.id AND state_item.deleted=0)
|
||||
THEN 'WAIT_LOADING' ELSE p.plan_status END planStatus,b.version version
|
||||
FROM fl_container_booking b
|
||||
LEFT JOIN fl_loading_plan p ON p.container_booking_id=b.id
|
||||
WHERE b.record_status='ACTIVE'
|
||||
AND (p.id IS NULL OR p.plan_status IN ('EDITING','LOADING'))
|
||||
AND b.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
<if test="query.warehouseId != null">AND b.warehouse_id=#{query.warehouseId}</if>
|
||||
<if test="query.customerId != null or query.inboundCode != null or query.expectedArrivalStart != null or query.expectedArrivalEnd != null or query.orderStatus != null">
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item filter_item
|
||||
JOIN fl_inbound_order filter_inbound ON filter_inbound.tenant_id=filter_item.account_id
|
||||
AND filter_inbound.id=filter_item.inbound_id
|
||||
WHERE filter_item.loading_plan_id=p.id AND filter_item.deleted=0
|
||||
<if test="query.customerId != null">AND filter_item.account_id=#{query.customerId}</if>
|
||||
<if test="query.inboundCode != null and query.inboundCode != ''">AND filter_item.inbound_code LIKE CONCAT('%',#{query.inboundCode},'%')</if>
|
||||
<if test="query.expectedArrivalStart != null">AND filter_inbound.expected_arrival_at >= #{query.expectedArrivalStart}</if>
|
||||
<if test="query.expectedArrivalEnd != null">AND filter_inbound.expected_arrival_at < DATE_ADD(#{query.expectedArrivalEnd},INTERVAL 1 DAY)</if>
|
||||
<if test="orderStatus != null">
|
||||
<choose>
|
||||
<when test="orderStatus == 'PENDING_REVIEW'">AND filter_inbound.order_status_snapshot IN ('SUBMITTED','PENDING_REVIEW')</when>
|
||||
<when test="orderStatus == 'WAIT_MEASURE'">AND filter_inbound.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING')</when>
|
||||
<when test="orderStatus == 'WAIT_SPLIT'">AND filter_inbound.process_status='WAIT_SPLIT'</when>
|
||||
<when test="orderStatus == 'LOADING'">AND filter_inbound.process_status='LOADING'</when>
|
||||
</choose>
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
ORDER BY b.updated_at DESC,b.id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<WorkbenchContainer> workbenchContainers(Page<WorkbenchContainer> page,
|
||||
@Param("query") WorkbenchContainerQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels,
|
||||
@Param("orderStatus") String orderStatus);
|
||||
|
||||
@Select("""
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
|
||||
|
||||
@@ -2,6 +2,10 @@ package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
@@ -19,6 +23,7 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** 智能装柜聚合的真实 MySQL 持久化边界。 */
|
||||
@Repository
|
||||
@@ -32,6 +37,13 @@ public class SmartLoadingRepository {
|
||||
@Resource private InboundOrderMapper inbounds;
|
||||
|
||||
public List<Candidate> candidates(long warehouseId) { return candidates.candidates(warehouseId); }
|
||||
public List<Candidate> globalCandidates(Long warehouseId) { return candidates.globalCandidates(warehouseId); }
|
||||
public PageResult<WorkbenchContainer> workbenchContainers(WorkbenchContainerQuery query,
|
||||
Set<String> actualChannels) {
|
||||
return PageResult.from(candidates.workbenchContainers(
|
||||
new Page<>(query.page(), query.size()), query, actualChannels,
|
||||
query.orderStatus() == null ? null : query.orderStatus().name()));
|
||||
}
|
||||
public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) {
|
||||
return candidates.channelContainerCounts(warehouseId);
|
||||
}
|
||||
|
||||
+5
-6
@@ -27,7 +27,6 @@ import java.util.Map;
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
public static final String DEFAULT_CALCULATION_MODE = "ADD";
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
@Resource
|
||||
@@ -116,7 +115,7 @@ public class CustomerDimensionConfigService {
|
||||
requirePositive(rule.innerLengthCm(), "箱规内长");
|
||||
requirePositive(rule.innerWidthCm(), "箱规内宽");
|
||||
requirePositive(rule.innerHeightCm(), "箱规内高");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规单侧加宽");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规尺寸增加值");
|
||||
if (rule.version() == null || rule.version() < 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "箱规版本号无效");
|
||||
}
|
||||
@@ -223,10 +222,10 @@ public class CustomerDimensionConfigService {
|
||||
}
|
||||
|
||||
private static RuleResponse ruleResponse(CustomerDimensionConfigEntity value) {
|
||||
BigDecimal edgePair = value.getEdgeAdditionCm().multiply(TWO);
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(edgePair);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(edgePair);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(edgePair);
|
||||
BigDecimal addition = value.getEdgeAdditionCm();
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(addition);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(addition);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(addition);
|
||||
BigDecimal volume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
|
||||
+18
-16
@@ -13,10 +13,9 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/** Resolves measured carton dimensions to the customer's smallest containing dimension rule. */
|
||||
/** Resolves dimensions to the smallest containing rule, capped at the largest configured tier. */
|
||||
@Service
|
||||
public class CustomerDimensionWeightingService {
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
private final CustomerDimensionConfigRepository repository;
|
||||
@@ -62,16 +61,8 @@ public class CustomerDimensionWeightingService {
|
||||
.filter(rule -> rule.contains(measured))
|
||||
.findFirst()
|
||||
.map(Rule::weighted)
|
||||
.orElseGet(() -> actualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
|
||||
private WeightedDimensions actualDimensions(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal rawVolume = lengthCm.multiply(widthCm).multiply(heightCm)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(null, null, null, lengthCm, widthCm, heightCm,
|
||||
rawVolume, rawVolume.setScale(3, RoundingMode.HALF_UP),
|
||||
"ACTUAL_DIMENSION_V1");
|
||||
.orElseGet(() -> rules.get(rules.size() - 1)
|
||||
.weightedActualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +104,21 @@ public class CustomerDimensionWeightingService {
|
||||
}
|
||||
|
||||
private WeightedDimensions weighted() {
|
||||
BigDecimal edgePair = edgeAdditionCm.multiply(TWO);
|
||||
BigDecimal outerLength = innerLengthCm.add(edgePair);
|
||||
BigDecimal outerWidth = innerWidthCm.add(edgePair);
|
||||
BigDecimal outerHeight = innerHeightCm.add(edgePair);
|
||||
BigDecimal outerLength = innerLengthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerWidth = innerWidthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerHeight = innerHeightCm.add(edgeAdditionCm);
|
||||
BigDecimal rawVolume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(boxType, version, edgeAdditionCm, outerLength, outerWidth, outerHeight,
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version);
|
||||
}
|
||||
|
||||
private WeightedDimensions weightedActualDimensions(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal outerLength = lengthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerWidth = widthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerHeight = heightCm.add(edgeAdditionCm);
|
||||
BigDecimal rawVolume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
|
||||
@@ -183,8 +183,20 @@ public class MeasurementBatchService {
|
||||
if (!itemNumbers.add(source.itemNo())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细号不能重复");
|
||||
}
|
||||
if ((source.outerLengthCm() != null && source.outerLengthCm().signum() <= 0)
|
||||
|| (source.outerWidthCm() != null && source.outerWidthCm().signum() <= 0)
|
||||
|| (source.outerHeightCm() != null && source.outerHeightCm().signum() <= 0)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"仓库修改外箱尺寸必须大于0,明细 " + source.itemNo());
|
||||
}
|
||||
boolean hasOuterOverride = source.outerLengthCm() != null
|
||||
|| source.outerWidthCm() != null
|
||||
|| source.outerHeightCm() != null;
|
||||
BigDecimal baseLength = source.outerLengthCm() != null ? source.outerLengthCm() : source.lengthCm();
|
||||
BigDecimal baseWidth = source.outerWidthCm() != null ? source.outerWidthCm() : source.widthCm();
|
||||
BigDecimal baseHeight = source.outerHeightCm() != null ? source.outerHeightCm() : source.heightCm();
|
||||
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolve(
|
||||
source.lengthCm(), source.widthCm(), source.heightCm());
|
||||
baseLength, baseWidth, baseHeight);
|
||||
String requestedMode = normalizeMode(source.coefficientMode());
|
||||
CustomerChargeRuleEntity coefficientRule = "MULTIPLY".equals(requestedMode)
|
||||
? requireCoefficientRule(command.tenantId(), customerId, source.chargeRuleId())
|
||||
@@ -195,32 +207,16 @@ public class MeasurementBatchService {
|
||||
boolean hasMatchedAddition = useDimensionRule && weighted.boxType() != null;
|
||||
// ADD 只从匹配箱规取得加边值,外尺寸仍以本次实测尺寸为基准,
|
||||
// 不能把箱规边界尺寸当成本次货物的实际外尺寸。
|
||||
BigDecimal outerLength = hasMatchedAddition ? source.lengthCm().add(addition) : source.lengthCm();
|
||||
BigDecimal outerWidth = hasMatchedAddition ? source.widthCm().add(addition) : source.widthCm();
|
||||
BigDecimal outerHeight = hasMatchedAddition ? source.heightCm().add(addition) : source.heightCm();
|
||||
boolean hasOuterOverride = source.outerLengthCm() != null
|
||||
|| source.outerWidthCm() != null
|
||||
|| source.outerHeightCm() != null;
|
||||
if ((source.outerLengthCm() != null && source.outerLengthCm().signum() <= 0)
|
||||
|| (source.outerWidthCm() != null && source.outerWidthCm().signum() <= 0)
|
||||
|| (source.outerHeightCm() != null && source.outerHeightCm().signum() <= 0)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"仓库修改外箱尺寸必须大于0,明细 " + source.itemNo());
|
||||
}
|
||||
BigDecimal outerLength = hasMatchedAddition ? baseLength.add(addition) : baseLength;
|
||||
BigDecimal outerWidth = hasMatchedAddition ? baseWidth.add(addition) : baseWidth;
|
||||
BigDecimal outerHeight = hasMatchedAddition ? baseHeight.add(addition) : baseHeight;
|
||||
if (coefficientRule != null) {
|
||||
BigDecimal specificationCoefficient = resolveSpecificationCoefficient(
|
||||
coefficientRule, source.lengthCm(), source.widthCm(), source.heightCm());
|
||||
coefficientRule, baseLength, baseWidth, baseHeight);
|
||||
outerLength = applyCoefficient(outerLength, specificationCoefficient);
|
||||
outerWidth = applyCoefficient(outerWidth, specificationCoefficient);
|
||||
outerHeight = applyCoefficient(outerHeight, specificationCoefficient);
|
||||
}
|
||||
if (hasOuterOverride) {
|
||||
// Each warehouse-confirmed dimension is authoritative on its own.
|
||||
// Unspecified dimensions keep their strategy-calculated values.
|
||||
if (source.outerLengthCm() != null) outerLength = source.outerLengthCm();
|
||||
if (source.outerWidthCm() != null) outerWidth = source.outerWidthCm();
|
||||
if (source.outerHeightCm() != null) outerHeight = source.outerHeightCm();
|
||||
}
|
||||
BigDecimal unitVolumeRaw = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = hasMatchedAddition && !hasOuterOverride
|
||||
@@ -262,9 +258,13 @@ public class MeasurementBatchService {
|
||||
: useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1");
|
||||
item.setSourceImageFileId(source.intelligentImageFileId());
|
||||
item.setOverLimitWarning(source.overLimitWarning());
|
||||
item.setOuterLengthCm(outerLength.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm(outerWidth.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm(outerHeight.setScale(3, RoundingMode.HALF_UP));
|
||||
// Override fields retain the operator's base values so repeated saves do not compound the strategy.
|
||||
item.setOuterLengthCm((source.outerLengthCm() != null ? source.outerLengthCm() : outerLength)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm((source.outerWidthCm() != null ? source.outerWidthCm() : outerWidth)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm((source.outerHeightCm() != null ? source.outerHeightCm() : outerHeight)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setRemark(source.remark());
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
return item;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
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.*;
|
||||
@@ -38,6 +39,17 @@ import java.util.*;
|
||||
@Service
|
||||
public class SmartLoadingService {
|
||||
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.10");
|
||||
private static final Map<String, Set<String>> WORKBENCH_CHANNEL_GROUPS = Map.of(
|
||||
"MATSON_CLX", Set.of("MATSON_CLX_TRUCK", "MATSON_CLX_PARCEL"),
|
||||
"MATSON_MAX", Set.of("MATSON_MAX_TRUCK", "MATSON_MAX_PARCEL"),
|
||||
"EXX", Set.of("EXX_TRUCK", "EXX_RAIL_NY"),
|
||||
"HEDE", Set.of("HEDE_TRUCK", "HEDE_PARCEL"),
|
||||
"OA_LA", Set.of("OA_LA_TRUCK", "OA_LA_FULL_TRUCK", "OA_PARCEL"),
|
||||
"OA_CHICAGO", Set.of("OA_CHICAGO_RAIL"),
|
||||
"OA_HOUSTON", Set.of("OA_HOUSTON_TRUCK"),
|
||||
"OA_SAVANNAH", Set.of("OA_SAVANNAH_TRUCK"),
|
||||
"OA_NEW_YORK_TRUCK", Set.of("OA_NEW_YORK_TRUCK")
|
||||
);
|
||||
@Resource private SmartLoadingRepository repository;
|
||||
@Resource private OrderCargoClient orderCargoClient;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@@ -96,6 +108,57 @@ public class SmartLoadingService {
|
||||
return grouped.values().stream().map(this::inboundCandidate).toList();
|
||||
}
|
||||
|
||||
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
|
||||
public PageResult<InboundCandidate> inboundCandidates(InboundCandidateQuery query) {
|
||||
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
|
||||
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
|
||||
throw validation("预计到货开始日期不能晚于结束日期");
|
||||
}
|
||||
Set<String> actualChannels = actualChannels(query.channelGroupCodes());
|
||||
List<InboundCandidate> all = repository.globalCandidates(query.warehouseId()).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()))
|
||||
.values().stream().map(this::inboundCandidate).toList();
|
||||
Map<String, GroupSnapshot> snapshots = new HashMap<>();
|
||||
Map<Long, List<InboundCandidate>> grouped = new LinkedHashMap<>();
|
||||
List<InboundCandidate> standalone = new ArrayList<>();
|
||||
for (InboundCandidate candidate : all) {
|
||||
GroupSnapshot group = snapshots.computeIfAbsent(candidate.accountId() + "|" + candidate.inboundId(),
|
||||
ignored -> group(candidate.accountId(), candidate.inboundId()));
|
||||
if (group == null) {
|
||||
standalone.add(candidate);
|
||||
} else {
|
||||
grouped.computeIfAbsent(group.groupId(), ignored -> new ArrayList<>()).add(candidate);
|
||||
}
|
||||
}
|
||||
List<InboundCandidate> units = new ArrayList<>(standalone);
|
||||
for (List<InboundCandidate> members : grouped.values()) {
|
||||
GroupSnapshot group = snapshots.get(members.get(0).accountId() + "|" + members.get(0).inboundId());
|
||||
InboundCandidate aggregate = globalGroupCandidate(group, members);
|
||||
if (aggregate != null) units.add(aggregate);
|
||||
}
|
||||
units = units.stream().filter(candidate -> matches(query, actualChannels, candidate))
|
||||
.sorted(Comparator.comparing(InboundCandidate::expectedArrivalAt,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(InboundCandidate::inboundId, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.toList();
|
||||
long total = units.size();
|
||||
long offset = query.page() - 1 > Long.MAX_VALUE / query.size()
|
||||
? Long.MAX_VALUE : (query.page() - 1) * query.size();
|
||||
long from = Math.min(total, offset);
|
||||
long to = Math.min(total, from + query.size());
|
||||
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||||
return new PageResult<>(units.subList((int) from, (int) to), query.page(), query.size(), total, pages);
|
||||
}
|
||||
|
||||
public PageResult<WorkbenchContainer> containers(WorkbenchContainerQuery query) {
|
||||
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
|
||||
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
|
||||
throw validation("预计到货开始日期不能晚于结束日期");
|
||||
}
|
||||
return repository.workbenchContainers(query, actualChannels(query.channelGroupCodes()));
|
||||
}
|
||||
|
||||
private InboundCandidate inboundCandidate(List<Candidate> details) {
|
||||
Candidate first = details.get(0);
|
||||
int packages = details.stream().map(Candidate::availablePackageCount)
|
||||
@@ -114,7 +177,93 @@ public class SmartLoadingService {
|
||||
first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(),
|
||||
averageVolume, volume, first.coLoadGroupId(), first.groupNo(), first.memberCount(),
|
||||
first.groupTotalBoxes(), first.locked(), first.blockReason(), first.orderStatusSnapshot(),
|
||||
first.expectedArrivalAt());
|
||||
first.expectedArrivalAt(), List.of());
|
||||
}
|
||||
|
||||
private InboundCandidate globalGroupCandidate(GroupSnapshot group, List<InboundCandidate> availableMembers) {
|
||||
if (group == null || !"ACTIVE".equals(group.status()) || group.lockedPlanId() != null) return null;
|
||||
Map<Long, InboundCandidate> byInbound = availableMembers.stream()
|
||||
.collect(java.util.stream.Collectors.toMap(InboundCandidate::inboundId, value -> value, (a, b) -> a));
|
||||
for (Member member : group.members()) {
|
||||
InboundCandidate candidate = byInbound.get(member.inboundId());
|
||||
if (candidate == null || !Objects.equals(candidate.accountId(), member.tenantId())
|
||||
|| !Objects.equals(candidate.availablePackageCount(), member.fullPackageCount())) return null;
|
||||
}
|
||||
List<InboundCandidate> ordered = group.members().stream().map(member -> byInbound.get(member.inboundId())).toList();
|
||||
InboundCandidate first = ordered.get(0);
|
||||
int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum();
|
||||
BigDecimal volume = ordered.stream().map(InboundCandidate::availableVolumeCbm)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP);
|
||||
BigDecimal gross = weightedAverage(ordered, InboundCandidate::unitGrossWeightKg, packages);
|
||||
BigDecimal net = weightedAverage(ordered, InboundCandidate::unitNetWeightKg, packages);
|
||||
BigDecimal averageVolume = packages == 0 ? BigDecimal.ZERO
|
||||
: volume.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
|
||||
List<GroupMemberCandidate> groupMembers = ordered.stream().map(member -> new GroupMemberCandidate(
|
||||
member.accountId(), member.inboundId(), member.orderId(), member.orderNo(), member.inboundCode(),
|
||||
member.customerName(), member.availablePackageCount(), member.orderStatusSnapshot(),
|
||||
member.expectedArrivalAt())).toList();
|
||||
String customers = ordered.stream().map(InboundCandidate::customerName).filter(StringUtils::hasText)
|
||||
.distinct().collect(java.util.stream.Collectors.joining("、"));
|
||||
String goods = ordered.stream().map(InboundCandidate::goodsSummary).filter(StringUtils::hasText)
|
||||
.distinct().collect(java.util.stream.Collectors.joining("、"));
|
||||
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
|
||||
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
customers, goods, packages, gross, net, averageVolume, volume, group.groupId(), group.groupNo(),
|
||||
group.members().size(), group.fullPackageCount(), false, null, first.orderStatusSnapshot(),
|
||||
ordered.stream().map(InboundCandidate::expectedArrivalAt).filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null),
|
||||
groupMembers);
|
||||
}
|
||||
|
||||
private static BigDecimal weightedAverage(List<InboundCandidate> members,
|
||||
java.util.function.Function<InboundCandidate, BigDecimal> getter,
|
||||
int packages) {
|
||||
if (packages == 0) return BigDecimal.ZERO;
|
||||
return members.stream().map(candidate -> unit(getter.apply(candidate)).multiply(
|
||||
BigDecimal.valueOf(candidate.availablePackageCount())))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private static Set<String> actualChannels(List<String> groups) {
|
||||
Set<String> actual = new LinkedHashSet<>();
|
||||
for (String value : groups) {
|
||||
String normalized = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
Set<String> mapped = WORKBENCH_CHANNEL_GROUPS.get(normalized);
|
||||
if (mapped == null) throw validation("不支持的渠道分组: " + value);
|
||||
actual.addAll(mapped);
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
private static boolean matches(InboundCandidateQuery query, Set<String> actualChannels,
|
||||
InboundCandidate candidate) {
|
||||
if (!actualChannels.contains(candidate.channelCode())) return false;
|
||||
if (query.customerId() != null && !Objects.equals(candidate.accountId(), query.customerId())
|
||||
&& candidate.groupMembers().stream().noneMatch(
|
||||
member -> Objects.equals(member.accountId(), query.customerId()))) return false;
|
||||
String inboundCode = trim(query.inboundCode());
|
||||
if (StringUtils.hasText(inboundCode) && candidate.groupMembers().stream()
|
||||
.map(GroupMemberCandidate::inboundCode).filter(Objects::nonNull)
|
||||
.noneMatch(value -> value.toLowerCase(Locale.ROOT).contains(inboundCode.toLowerCase(Locale.ROOT)))
|
||||
&& (candidate.inboundCode() == null || !candidate.inboundCode().toLowerCase(Locale.ROOT)
|
||||
.contains(inboundCode.toLowerCase(Locale.ROOT)))) return false;
|
||||
if (query.expectedArrivalStart() != null || query.expectedArrivalEnd() != null) {
|
||||
boolean arrivalMatches = candidate.groupMembers().isEmpty()
|
||||
? inArrivalRange(candidate.expectedArrivalAt(), query)
|
||||
: candidate.groupMembers().stream().anyMatch(member -> inArrivalRange(
|
||||
member.expectedArrivalAt(), query));
|
||||
if (!arrivalMatches) return false;
|
||||
}
|
||||
return query.orderStatus() == null || query.orderStatus() == CandidateOrderStatus.WAIT_SPLIT;
|
||||
}
|
||||
|
||||
private static boolean inArrivalRange(LocalDateTime arrival, InboundCandidateQuery query) {
|
||||
if (arrival == null) return false;
|
||||
return (query.expectedArrivalStart() == null
|
||||
|| !arrival.toLocalDate().isBefore(query.expectedArrivalStart()))
|
||||
&& (query.expectedArrivalEnd() == null
|
||||
|| !arrival.toLocalDate().isAfter(query.expectedArrivalEnd()));
|
||||
}
|
||||
|
||||
public Plan draftByBooking(long bookingId) {
|
||||
|
||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
|
||||
@@ -56,7 +56,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
|
||||
@@ -861,7 +861,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
|
||||
@@ -483,16 +483,72 @@ paths:
|
||||
'403': {description: 无智能装柜查询权限}
|
||||
/api/platform/v1/first-leg/smart-loading/inbound-candidates:
|
||||
get:
|
||||
summary: 按入仓码查询智能装柜候选
|
||||
summary: 全局分页查询智能装柜候选
|
||||
description: >-
|
||||
需要 platform:firstleg:smart-loading:list 权限。每个入库单/入仓码只返回一行;
|
||||
需要 platform:firstleg:smart-loading:list 权限。查询不绑定柜次;渠道、仓库、客户、入仓码、
|
||||
预计到货日期和订单状态共同作用于全局候选。每个普通入库单返回一行;同柜不可拆组作为一个
|
||||
分页单元返回,groupMembers 包含完整成员,不会跨页或因单个成员命中搜索条件而拆散。
|
||||
平均单箱毛重、净重按整票实测重量除以总箱数,平均单箱体积按剩余规格数量加权。
|
||||
响应携带订单状态快照 orderStatusSnapshot 与预计到货时间 expectedArrivalAt,供平台筛选候选入仓码;
|
||||
这些数据用于数量级计划,不表示具体箱号已核验归属。
|
||||
只返回当前具备实际装柜候选资格的 WAIT_SPLIT 数据;PENDING_REVIEW、WAIT_MEASURE、LOADING
|
||||
是统一筛选枚举,但在候选接口中返回空页。LOADED、COMPLETED 与已锁定或成员不完整的组永不返回。
|
||||
分配时仍由目标 planId 接口复核真实渠道、仓库、容量、组锁和并发版本。
|
||||
parameters:
|
||||
- {name: bookingId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: warehouseId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses: {'200': {description: 成功,data 为入仓码级候选列表}}
|
||||
- name: channelGroupCodes
|
||||
in: query
|
||||
required: true
|
||||
description: >-
|
||||
至少一项,可重复传参。分组到实际渠道:MATSON_CLX=[MATSON_CLX_TRUCK,MATSON_CLX_PARCEL];
|
||||
MATSON_MAX=[MATSON_MAX_TRUCK,MATSON_MAX_PARCEL];EXX=[EXX_TRUCK,EXX_RAIL_NY];
|
||||
HEDE=[HEDE_TRUCK,HEDE_PARCEL];OA_LA=[OA_LA_TRUCK,OA_LA_FULL_TRUCK,OA_PARCEL];
|
||||
OA_CHICAGO=[OA_CHICAGO_RAIL];OA_HOUSTON=[OA_HOUSTON_TRUCK];
|
||||
OA_SAVANNAH=[OA_SAVANNAH_TRUCK];OA_NEW_YORK_TRUCK=[OA_NEW_YORK_TRUCK]。
|
||||
schema:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 9
|
||||
uniqueItems: true
|
||||
items: {type: string, enum: [MATSON_CLX, MATSON_MAX, EXX, HEDE, OA_LA, OA_CHICAGO, OA_HOUSTON, OA_SAVANNAH, OA_NEW_YORK_TRUCK]}
|
||||
- {name: warehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 40}}
|
||||
- {name: expectedArrivalStart, in: query, required: false, schema: {type: string, format: date}}
|
||||
- {name: expectedArrivalEnd, in: query, required: false, schema: {type: string, format: date}}
|
||||
- {name: orderStatus, in: query, required: false, schema: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING]}}
|
||||
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功,data 为标准分页;total/pages 按普通入库单或完整同柜组分页单元统计
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingInboundCandidatePageEnvelope'}}}
|
||||
/api/platform/v1/first-leg/smart-loading/containers:
|
||||
get:
|
||||
summary: 分页查询智能装柜工作台柜次
|
||||
description: >-
|
||||
需要 platform:firstleg:smart-loading:list 权限。与全局候选使用相同的渠道、仓库、客户、入仓码、
|
||||
预计到货日期及订单状态筛选;客户及入仓事实以柜内任一有效装柜明细命中为准,查询不会修改或拆散同柜组。
|
||||
只返回 WAIT_LOADING、EDITING 或 LOADING 的 ACTIVE 柜次,COMPLETED 永不返回。
|
||||
parameters:
|
||||
- name: channelGroupCodes
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 9
|
||||
uniqueItems: true
|
||||
items: {type: string, enum: [MATSON_CLX, MATSON_MAX, EXX, HEDE, OA_LA, OA_CHICAGO, OA_HOUSTON, OA_SAVANNAH, OA_NEW_YORK_TRUCK]}
|
||||
- {name: warehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 40}}
|
||||
- {name: expectedArrivalStart, in: query, required: false, schema: {type: string, format: date}}
|
||||
- {name: expectedArrivalEnd, in: query, required: false, schema: {type: string, format: date}}
|
||||
- {name: orderStatus, in: query, required: false, schema: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING]}}
|
||||
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功,柜次按更新时间、技术ID倒序稳定分页
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingWorkbenchContainerPageEnvelope'}}}
|
||||
/api/platform/v1/first-leg/loading-associations/summary:
|
||||
get:
|
||||
summary: 查询入仓片段的装柜与国内PDA关联摘要
|
||||
@@ -1203,10 +1259,10 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/UnifiedInboundSummary'}}
|
||||
page: {type: integer, format: int64, minimum: 1}
|
||||
size: {type: integer, format: int64, minimum: 1}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
page: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
size: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
total: {type: string, pattern: '^[0-9]+$'}
|
||||
pages: {type: string, pattern: '^[0-9]+$'}
|
||||
UnifiedInboundListEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1387,7 +1443,7 @@ components:
|
||||
minimum: 0.001
|
||||
maximum: 100
|
||||
multipleOf: 0.001
|
||||
description: 箱规长、宽、高三个方向分别使用的每侧加宽厘米数
|
||||
description: 箱规长、宽、高三个方向分别增加一次的厘米数
|
||||
example: 2.000
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
version: {type: integer, minimum: 0, description: 乐观锁版本号}
|
||||
@@ -1413,9 +1469,9 @@ components:
|
||||
innerWidthCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
innerHeightCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
edgeAdditionCm: {type: number, minimum: 0.001, maximum: 100, example: 2.000}
|
||||
outerLengthCm: {type: number, minimum: 0.001, description: innerLengthCm + edgeAdditionCm * 2}
|
||||
outerWidthCm: {type: number, minimum: 0.001, description: innerWidthCm + edgeAdditionCm * 2}
|
||||
outerHeightCm: {type: number, minimum: 0.001, description: innerHeightCm + edgeAdditionCm * 2}
|
||||
outerLengthCm: {type: number, minimum: 0.001, description: innerLengthCm + edgeAdditionCm}
|
||||
outerWidthCm: {type: number, minimum: 0.001, description: innerWidthCm + edgeAdditionCm}
|
||||
outerHeightCm: {type: number, minimum: 0.001, description: innerHeightCm + edgeAdditionCm}
|
||||
volumeCbm: {type: number, multipleOf: 0.001, description: 使用未舍入外径完整计算后四舍五入保留3位}
|
||||
remark: {type: string, nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
@@ -1880,6 +1936,103 @@ components:
|
||||
candidateInboundCount: {type: integer, format: int64, minimum: 0, description: 当前可分配的唯一入仓码数量}
|
||||
availablePackageCount: {type: integer, format: int64, minimum: 0, description: 当前候选规格剩余可分配箱数合计}
|
||||
availableContainerCount: {type: integer, format: int64, minimum: 0, description: 当前 WAIT_LOADING、EDITING 或 LOADING 可编排柜次数量}
|
||||
SmartLoadingInboundCandidatePageEnvelope:
|
||||
type: object
|
||||
required: [code, message, data]
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/SmartLoadingInboundCandidate'}}
|
||||
page: {type: integer, format: int64, minimum: 1}
|
||||
size: {type: integer, format: int64, minimum: 1}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
SmartLoadingInboundCandidate:
|
||||
type: object
|
||||
required: [accountId, customerId, inboundId, orderId, orderNo, inboundCode, warehouseId, channelCode,
|
||||
channelName, customerName, goodsSummary, availablePackageCount, unitGrossWeightKg,
|
||||
unitNetWeightKg, averageUnitVolumeCbm, availableVolumeCbm, locked, groupMembers]
|
||||
properties:
|
||||
accountId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$', description: 与 accountId 相同的客户技术关联ID}
|
||||
inboundId: {type: string, pattern: '^[1-9][0-9]*$', description: 普通候选ID;组聚合行中为可用于整组分配的稳定代表成员ID}
|
||||
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
orderNo: {type: string}
|
||||
inboundCode: {type: string}
|
||||
orderStatusSnapshot: {type: string, nullable: true}
|
||||
expectedArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
channelCode: {type: string}
|
||||
channelName: {type: string}
|
||||
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
destinationWarehouseCode: {type: string, nullable: true}
|
||||
destinationWarehouseName: {type: string, nullable: true}
|
||||
customerName: {type: string}
|
||||
goodsSummary: {type: string}
|
||||
availablePackageCount: {type: integer, minimum: 1, description: 普通行剩余箱数;组行完整成员箱数合计}
|
||||
unitGrossWeightKg: {type: number, minimum: 0}
|
||||
unitNetWeightKg: {type: number, minimum: 0}
|
||||
averageUnitVolumeCbm: {type: number, minimum: 0}
|
||||
availableVolumeCbm: {type: number, minimum: 0}
|
||||
coLoadGroupId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
groupNo: {type: string, nullable: true}
|
||||
memberCount: {type: integer, minimum: 2, nullable: true}
|
||||
groupTotalBoxes: {type: integer, minimum: 1, nullable: true}
|
||||
locked: {type: boolean}
|
||||
blockReason: {type: string, nullable: true}
|
||||
groupMembers:
|
||||
type: array
|
||||
description: 同柜组聚合行的完整成员;普通候选为空数组
|
||||
items:
|
||||
type: object
|
||||
required: [accountId, inboundId, orderId, orderNo, inboundCode, customerName, availablePackageCount]
|
||||
properties:
|
||||
accountId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
inboundId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
orderNo: {type: string}
|
||||
inboundCode: {type: string}
|
||||
customerName: {type: string}
|
||||
availablePackageCount: {type: integer, minimum: 1}
|
||||
orderStatusSnapshot: {type: string, nullable: true}
|
||||
expectedArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
SmartLoadingWorkbenchContainerPageEnvelope:
|
||||
type: object
|
||||
required: [code, message, data]
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/SmartLoadingWorkbenchContainer'}}
|
||||
page: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
size: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
total: {type: string, pattern: '^[0-9]+$'}
|
||||
pages: {type: string, pattern: '^[0-9]+$'}
|
||||
SmartLoadingWorkbenchContainer:
|
||||
type: object
|
||||
required: [bookingId, bookingNo, cabinetNo, warehouseId, warehouseName, channelCode,
|
||||
channelName, containerType, planStatus, version]
|
||||
properties:
|
||||
bookingId: {type: string, pattern: '^[1-9][0-9]*$', description: containerBookingId 技术关联}
|
||||
bookingNo: {type: string, description: 货柜登记业务号}
|
||||
cabinetNo: {type: string, description: 稳定柜次业务号}
|
||||
containerNo: {type: string, nullable: true, description: 真实集装箱号,允许在装柜前后补}
|
||||
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
warehouseName: {type: string}
|
||||
channelCode: {type: string}
|
||||
channelName: {type: string}
|
||||
containerType: {type: string}
|
||||
planId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
planNo: {type: string, nullable: true}
|
||||
planStatus: {type: string, enum: [WAIT_LOADING, EDITING, LOADING]}
|
||||
version: {type: integer, minimum: 0, description: 柜次版本}
|
||||
SmartLoadingPlan:
|
||||
type: object
|
||||
required: [id, planNo, status, bookingId, version, canAssign, canEdit, finalCompletionStatus, items]
|
||||
|
||||
+19
-7
@@ -4,6 +4,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import java.lang.reflect.Method;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -12,16 +13,27 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
authority("draftByBooking","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class);
|
||||
authority("detail","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class);
|
||||
authority("candidates","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class,long.class);
|
||||
authority("inboundCandidates","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class,long.class);
|
||||
authority("inboundCandidates","platform:firstleg:smart-loading:list",AuthenticatedUser.class,InboundCandidateQuery.class);
|
||||
authority("containers","platform:firstleg:smart-loading:list",AuthenticatedUser.class,WorkbenchContainerQuery.class);
|
||||
authority("channels","platform:firstleg:smart-loading:list",AuthenticatedUser.class,long.class);
|
||||
}
|
||||
@Test void openApiRequiresBookingAndWarehouseForCandidates() throws Exception {
|
||||
@Test void openApiFreezesFilteredWorkbenchContainers() throws Exception {
|
||||
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
|
||||
assertThat(yaml).contains("/api/platform/v1/first-leg/smart-loading/candidates:")
|
||||
.contains("/api/platform/v1/first-leg/smart-loading/channels:")
|
||||
.contains("零候选或零可用柜次的有效渠道仍返回")
|
||||
.contains("name: bookingId, in: query, required: true")
|
||||
.contains("name: warehouseId, in: query, required: true");
|
||||
assertThat(yaml).contains("/api/platform/v1/first-leg/smart-loading/containers:")
|
||||
.contains("SmartLoadingWorkbenchContainerPageEnvelope:")
|
||||
.contains("只返回 WAIT_LOADING、EDITING 或 LOADING");
|
||||
}
|
||||
@Test void openApiFreezesGlobalPagedInboundCandidates() throws Exception {
|
||||
Method method=PlatformSmartLoadingController.class.getDeclaredMethod(
|
||||
"inboundCandidates",AuthenticatedUser.class,InboundCandidateQuery.class);
|
||||
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly("/inbound-candidates");
|
||||
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
|
||||
String contract=yaml.substring(yaml.indexOf("/api/platform/v1/first-leg/smart-loading/inbound-candidates:"),
|
||||
yaml.indexOf("/api/platform/v1/first-leg/loading-associations/summary:"));
|
||||
assertThat(contract).contains("name: channelGroupCodes")
|
||||
.contains("name: warehouseId")
|
||||
.contains("SmartLoadingInboundCandidatePageEnvelope")
|
||||
.doesNotContain("name: bookingId");
|
||||
}
|
||||
@Test void everyMutationDeclaresDedicatedPermission() throws Exception {
|
||||
authority("assign","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,AssignRequest.class);
|
||||
|
||||
+32
@@ -6,6 +6,9 @@ import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -64,6 +67,35 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.isLessThan(sql.indexOf("o.order_status_snapshot orderStatusSnapshot"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalCandidateQueryHasOptionalWarehouseAndNoContainerJoin() throws Exception {
|
||||
Method list = SmartLoadingCandidateMapper.class.getDeclaredMethod("globalCandidates", Long.class);
|
||||
String sql = String.join(" ", list.getAnnotation(Select.class).value());
|
||||
|
||||
assertThat(sql)
|
||||
.contains("#{warehouseId} IS NULL OR o.warehouse_id=#{warehouseId}")
|
||||
.contains("o.process_status='WAIT_SPLIT'")
|
||||
.doesNotContain("fl_container_booking")
|
||||
.doesNotContain("bookingId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void workbenchContainerQueryExcludesCompletedAndMatchesRelatedInboundFacts() throws Exception {
|
||||
Method list = SmartLoadingCandidateMapper.class.getDeclaredMethod("workbenchContainers",
|
||||
Page.class, WorkbenchContainerQuery.class, Set.class, String.class);
|
||||
String sql = String.join(" ", list.getAnnotation(Select.class).value());
|
||||
|
||||
assertThat(sql)
|
||||
.contains("p.plan_status IN ('EDITING','LOADING')")
|
||||
.contains("b.channel_code IN")
|
||||
.contains("fl_loading_plan_item filter_item")
|
||||
.contains("fl_inbound_order filter_inbound")
|
||||
.contains("filter_item.inbound_code LIKE")
|
||||
.contains("filter_inbound.expected_arrival_at")
|
||||
.contains("filter_inbound.process_status='LOADING'")
|
||||
.doesNotContain("p.plan_status IN ('EDITING','LOADING','COMPLETED')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailUsesSameBatchRule() throws Exception {
|
||||
Method detail = SmartLoadingCandidateMapper.class.getDeclaredMethod(
|
||||
|
||||
+9
-9
@@ -90,10 +90,10 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.scannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
assertThat(insertedItem.get().getScannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(30001L);
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1870");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("64.000");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1680");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("62.000");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_THIRD_PARTY);
|
||||
assertThat(measurement.getSourceCode()).isEqualTo("DEVICE");
|
||||
}
|
||||
@@ -155,16 +155,16 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.created()).isFalse();
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(4);
|
||||
assertThat(existing.getLengthCm()).isEqualByComparingTo("59.500");
|
||||
assertThat(existing.getUnitVolumeCbm()).isEqualByComparingTo("0.187");
|
||||
assertThat(existing.getTotalVolumeCbm()).isEqualByComparingTo("0.7480");
|
||||
assertThat(existing.getVolumeWeightKg()).isEqualByComparingTo("124.916");
|
||||
assertThat(existing.getUnitVolumeCbm()).isEqualByComparingTo("0.168");
|
||||
assertThat(existing.getTotalVolumeCbm()).isEqualByComparingTo("0.6720");
|
||||
assertThat(existing.getVolumeWeightKg()).isEqualByComparingTo("112.224");
|
||||
assertThat(existing.getSourceImageFileId()).isEqualTo(99001L);
|
||||
assertThat(existing.getScannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
verify(images).delete("internal-token", 90001L, 10001L, 70001L, 88001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanOutsideConfiguredWeightingRulesKeepsImageAndActualDimensions() {
|
||||
void scanOutsideConfiguredWeightingRulesKeepsImageAndUsesLargestTierAddition() {
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
|
||||
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null);
|
||||
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
|
||||
@@ -183,7 +183,7 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(insertedItem.get().getSourceImageFileId()).isEqualTo(99001L);
|
||||
assertThat(insertedItem.get().getLengthCm()).isEqualByComparingTo("60.100");
|
||||
assertThat(insertedItem.get().getCalculationRuleVersion())
|
||||
.isEqualTo("ACTUAL_DIMENSION_V1");
|
||||
.isEqualTo("CUSTOMER_DIMENSION_EXTRA_LARGE_V0");
|
||||
assertThat(insertedItem.get().getOverLimitWarning()).isNull();
|
||||
verify(images, never()).delete(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE));
|
||||
}
|
||||
|
||||
+4
-4
@@ -95,10 +95,10 @@ class CustomerDimensionConfigServiceTest {
|
||||
assertThat(result.rules()).extracting(rule -> rule.boxType())
|
||||
.containsExactly(CustomerDimensionBoxType.values());
|
||||
assertThat(result.rules().get(0).boxTypeName()).isEqualTo("小箱");
|
||||
assertThat(result.rules().get(0).outerLengthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(result.rules().get(0).outerWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(result.rules().get(0).outerHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(result.rules().get(0).volumeCbm()).isEqualByComparingTo("0.020");
|
||||
assertThat(result.rules().get(0).outerLengthCm()).isEqualByComparingTo("32.000");
|
||||
assertThat(result.rules().get(0).outerWidthCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(result.rules().get(0).outerHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(result.rules().get(0).volumeCbm()).isEqualByComparingTo("0.015");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class CustomerDimensionPricingFlowTest {
|
||||
var kgEstimate = pricing.estimate(rate("KG", "8.700000"), actualWeight, twoCartonsVolume);
|
||||
var cbmEstimate = pricing.estimate(rate("CBM", "1400.000000"), actualWeight, twoCartonsVolume);
|
||||
|
||||
assertThat(twoCartonsVolume).isEqualByComparingTo("0.040");
|
||||
assertThat(twoCartonsVolume).isEqualByComparingTo("0.030");
|
||||
assertThat(kgEstimate.quantity()).isEqualByComparingTo("24.000000");
|
||||
assertThat(kgEstimate.amount()).isEqualByComparingTo("208.80");
|
||||
assertThat(cbmEstimate.quantity()).isEqualByComparingTo("0.066116");
|
||||
|
||||
+14
-13
@@ -26,16 +26,16 @@ class CustomerDimensionWeightingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void measuredDimensionsUseSmallestContainingRuleAndAddEachEdgeTwice() {
|
||||
void measuredDimensionsUseSmallestContainingRuleAndAddConfiguredValueOncePerDimension() {
|
||||
var weighted = service.load(10001L).resolve(
|
||||
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("10"));
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.SMALL);
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(weighted.rawVolumeCbm()).isEqualByComparingTo("0.019584");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.020");
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("32.000");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(weighted.rawVolumeCbm()).isEqualByComparingTo("0.015488");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.015");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,19 +52,20 @@ class CustomerDimensionWeightingServiceTest {
|
||||
new BigDecimal("30.001"), new BigDecimal("20"), new BigDecimal("20"));
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.MEDIUM);
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.051");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.043");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dimensionsOutsideConfiguredWeightingRulesUseActualDimensions() {
|
||||
void dimensionsOutsideConfiguredWeightingRulesUseLargestTierAdditionOnActualDimensions() {
|
||||
var weighted = service.load(10001L).resolve(
|
||||
new BigDecimal("60.001"), new BigDecimal("50"), new BigDecimal("50"));
|
||||
|
||||
assertThat(weighted.boxType()).isNull();
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("60.001");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("50");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("50");
|
||||
assertThat(weighted.calculationRuleVersion()).isEqualTo("ACTUAL_DIMENSION_V1");
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.EXTRA_LARGE);
|
||||
assertThat(weighted.edgeAdditionCm()).isEqualByComparingTo("2.000");
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("62.001");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(weighted.calculationRuleVersion()).isEqualTo("CUSTOMER_DIMENSION_EXTRA_LARGE_V3");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-4
@@ -24,13 +24,13 @@ class CustomerDimensionWeightingTerminalRulesTest {
|
||||
var ruleSet = service.load(10001L);
|
||||
|
||||
assertWeighted(ruleSet, "10", "10", "10", CustomerDimensionBoxType.SMALL,
|
||||
"34", "24", "24", "0.020");
|
||||
"32", "22", "22", "0.015");
|
||||
assertWeighted(ruleSet, "35", "25", "25", CustomerDimensionBoxType.MEDIUM,
|
||||
"44", "34", "34", "0.051");
|
||||
"42", "32", "32", "0.043");
|
||||
assertWeighted(ruleSet, "45", "35", "35", CustomerDimensionBoxType.LARGE,
|
||||
"54", "44", "44", "0.105");
|
||||
"52", "42", "42", "0.092");
|
||||
assertWeighted(ruleSet, "55", "45", "45", CustomerDimensionBoxType.EXTRA_LARGE,
|
||||
"64", "54", "54", "0.187");
|
||||
"62", "52", "52", "0.168");
|
||||
}
|
||||
|
||||
private static void assertWeighted(CustomerDimensionWeightingService.RuleSet rules,
|
||||
|
||||
+76
-11
@@ -152,8 +152,8 @@ class MeasurementBatchServiceTest {
|
||||
+ "LARGE:50,40,40;系数:1.3;大箱:—;EXTRA_LARGE:60,50,50;系数:1.4;特大箱:—");
|
||||
when(pricingRepository.findDefaultRule(1L, 1L)).thenReturn(rule);
|
||||
MeasurementBatchCommand.Item small = new MeasurementBatchCommand.Item(
|
||||
80001L, "001", "A", 1, new BigDecimal("30"), new BigDecimal("20"),
|
||||
new BigDecimal("20"), 500L, null, null, null, null, null, null, "小箱");
|
||||
80001L, "001", "A", 1, new BigDecimal("10"), new BigDecimal("10"),
|
||||
new BigDecimal("10"), 500L, null, null, null, null, null, null, "低于最小档");
|
||||
MeasurementBatchCommand.Item medium = new MeasurementBatchCommand.Item(
|
||||
80002L, "002", "B", 1, new BigDecimal("31"), new BigDecimal("20"),
|
||||
new BigDecimal("20"), 501L, null, null, null, null, null, null, "中箱");
|
||||
@@ -166,9 +166,9 @@ class MeasurementBatchServiceTest {
|
||||
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
assertThat(items.getValue().get(0).getOuterLengthCm()).isEqualByComparingTo("33.000");
|
||||
assertThat(items.getValue().get(0).getOuterWidthCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(items.getValue().get(0).getOuterHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(items.getValue().get(0).getOuterLengthCm()).isEqualByComparingTo("11.000");
|
||||
assertThat(items.getValue().get(0).getOuterWidthCm()).isEqualByComparingTo("11.000");
|
||||
assertThat(items.getValue().get(0).getOuterHeightCm()).isEqualByComparingTo("11.000");
|
||||
assertThat(items.getValue().get(1).getOuterLengthCm()).isEqualByComparingTo("37.200");
|
||||
assertThat(items.getValue().get(1).getOuterWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(items.getValue().get(1).getOuterHeightCm()).isEqualByComparingTo("24.000");
|
||||
@@ -221,7 +221,32 @@ class MeasurementBatchServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseOuterDimensionsOverrideCalculatedCartonAndAllDerivedValues() {
|
||||
void dimensionsAboveLargestTierAddLargestTierValueToMeasuredDimensions() {
|
||||
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
|
||||
80001L, "001", "A", 2, new BigDecimal("60.001"), new BigDecimal("50"),
|
||||
new BigDecimal("50"), 500L, null, null, null, null, null, "ADD", "超过最大箱规");
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
1L, 10L, null, 0, "MANUAL", "PLATFORM", null, null, null,
|
||||
LocalDateTime.now(), null, null, null, null, null, 9L, List.of(source));
|
||||
|
||||
service.saveBatch(command);
|
||||
|
||||
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("62.001");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("270.001");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.168");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.3360");
|
||||
assertThat(calculated.getCalculationRuleVersion())
|
||||
.isEqualTo("CUSTOMER_DIMENSION_EXTRA_LARGE_V0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseOuterDimensionsBecomeBaseValuesBeforeAddStrategy() {
|
||||
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
|
||||
80001L, "001", "A", 2, new BigDecimal("40"), new BigDecimal("30"),
|
||||
new BigDecimal("20"), 500L, null, new BigDecimal("50"),
|
||||
@@ -239,10 +264,10 @@ class MeasurementBatchServiceTest {
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("50.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("40.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("30.000");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("190.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0600");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.1200");
|
||||
assertThat(calculated.getVolumeWeightKg()).isEqualByComparingTo("20.040");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("200.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0699");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.1398");
|
||||
assertThat(calculated.getVolumeWeightKg()).isEqualByComparingTo("23.347");
|
||||
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_V1");
|
||||
}
|
||||
|
||||
@@ -265,7 +290,47 @@ class MeasurementBatchServiceTest {
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("50.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("32.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0352");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0367");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.0734");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("160.000");
|
||||
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_L_V1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseOverrideUsesTheTierMatchedByItsBaseDimensionsBeforeMultiplyStrategy() {
|
||||
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
|
||||
rule.setId(79L);
|
||||
rule.setTenantId(1L);
|
||||
rule.setCustomerId(1L);
|
||||
rule.setRuleCode("TIERED-OVERRIDE");
|
||||
rule.setRuleVersion(1);
|
||||
rule.setStatus("ACTIVE");
|
||||
rule.setDefaultRule(true);
|
||||
rule.setLengthFactor(new BigDecimal("1.1"));
|
||||
rule.setWidthFactor(new BigDecimal("1.1"));
|
||||
rule.setHeightFactor(new BigDecimal("1.1"));
|
||||
rule.setRemark("SMALL:30,20,20;系数:1.1;小箱:—;MEDIUM:40,30,30;系数:1.2;中箱:—");
|
||||
when(pricingRepository.findDefaultRule(1L, 1L)).thenReturn(rule);
|
||||
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
|
||||
80001L, "001", "A", 1, new BigDecimal("30"), new BigDecimal("20"),
|
||||
new BigDecimal("20"), 500L, null, new BigDecimal("31"),
|
||||
null, null, null, "MULTIPLY", "仅修改长度");
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
1L, 10L, null, 0, "MANUAL", "PLATFORM", null, null, null,
|
||||
LocalDateTime.now(), null, null, null, null, null, 9L, List.of(source));
|
||||
|
||||
service.saveBatch(command);
|
||||
|
||||
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("31.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0215");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("133.200");
|
||||
assertThat(calculated.getChargeRuleId()).isEqualTo(79L);
|
||||
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_L_V1");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@ import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignRequest;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.AdjustRequest;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.CandidateOrderStatus;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.DeleteRequest;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.GroupMemberCandidate;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Plan;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.RecommendRequest;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WarehouseRemarkRequest;
|
||||
@@ -39,6 +44,7 @@ import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.sql.SQLException;
|
||||
@@ -144,6 +150,89 @@ class SmartLoadingServiceTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesGlobalCandidatesWithoutBindingAContainer() {
|
||||
Candidate clx = withChannel(candidate(9L, 10L, 20L, 30L, 4),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡");
|
||||
Candidate max = withChannel(candidate(19L, 11L, 21L, 31L, 6),
|
||||
"MATSON_MAX_TRUCK", "美森加班海卡");
|
||||
when(repository.globalCandidates(null)).thenReturn(List.of(clx, max));
|
||||
|
||||
var result = service.inboundCandidates(new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), null, null, null, null, null, null, 1, 20));
|
||||
|
||||
assertThat(result.total()).isEqualTo(1);
|
||||
assertThat(result.pages()).isEqualTo(1);
|
||||
assertThat(result.records()).singleElement().satisfies(candidate -> {
|
||||
assertThat(candidate.inboundId()).isEqualTo(10L);
|
||||
assertThat(candidate.channelCode()).isEqualTo("MATSON_CLX_TRUCK");
|
||||
});
|
||||
verify(repository).globalCandidates(null);
|
||||
verify(repository, never()).booking(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesWorkbenchContainersWithTheSameGlobalFilters() {
|
||||
WorkbenchContainer container = new WorkbenchContainer(99L, "CB-1", "CAB-1", null,
|
||||
100L, "义乌仓", "MATSON_CLX_TRUCK", "美森正班海卡", "40HC",
|
||||
1L, "LP-1", "EDITING", 2);
|
||||
WorkbenchContainerQuery query = new WorkbenchContainerQuery(List.of("MATSON_CLX"), 100L,
|
||||
9L, "IN-10", LocalDate.parse("2026-09-01"), LocalDate.parse("2026-09-30"),
|
||||
CandidateOrderStatus.WAIT_SPLIT, 1, 20);
|
||||
when(repository.workbenchContainers(eq(query), any())).thenReturn(
|
||||
new com.baiyue.common.core.api.PageResult<>(List.of(container), 1, 20, 1, 1));
|
||||
|
||||
var result = service.containers(query);
|
||||
|
||||
assertThat(result.records()).containsExactly(container);
|
||||
@SuppressWarnings("unchecked") ArgumentCaptor<java.util.Set<String>> channels =
|
||||
ArgumentCaptor.forClass(java.util.Set.class);
|
||||
verify(repository).workbenchContainers(eq(query), channels.capture());
|
||||
assertThat(channels.getValue()).containsExactlyInAnyOrder(
|
||||
"MATSON_CLX_TRUCK", "MATSON_CLX_PARCEL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void treatsWholeColoadGroupAsOneStablePagingUnit() {
|
||||
Candidate first = withArrival(withChannel(candidate(9L, 10L, 20L, 30L, 4),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-01T09:00:00");
|
||||
Candidate second = withArrival(withChannel(candidate(19L, 11L, 21L, 31L, 6),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-10T09:00:00");
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, "I10", 4), new Member(19L, 31L, 11L, "I11", 6));
|
||||
when(repository.globalCandidates(100L)).thenReturn(List.of(first, second));
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
|
||||
|
||||
var result = service.inboundCandidates(new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), 100L, null, null, LocalDate.parse("2026-09-10"),
|
||||
LocalDate.parse("2026-09-10"), null, 1, 1));
|
||||
|
||||
assertThat(result.total()).isEqualTo(1);
|
||||
assertThat(result.records()).singleElement().satisfies(candidate -> {
|
||||
assertThat(candidate.coLoadGroupId()).isEqualTo(700L);
|
||||
assertThat(candidate.availablePackageCount()).isEqualTo(10);
|
||||
assertThat(candidate.groupMembers()).extracting(GroupMemberCandidate::inboundId)
|
||||
.containsExactly(10L, 11L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludesIncompleteColoadGroupInsteadOfPublishingPartialMember() {
|
||||
Candidate first = withChannel(candidate(9L, 10L, 20L, 30L, 4),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡");
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, "I10", 4), new Member(19L, 31L, 11L, "I11", 6));
|
||||
when(repository.globalCandidates(100L)).thenReturn(List.of(first));
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
|
||||
var result = service.inboundCandidates(new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), 100L, null, null, null, null, null, 1, 20));
|
||||
|
||||
assertThat(result.records()).isEmpty();
|
||||
assertThat(result.total()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void channelDirectoryKeepsActiveZeroStatChannelAndAggregatesCandidateAndContainerFacts() {
|
||||
ChannelRow maxTruck = channel("MATSON_MAX_TRUCK", "美森加班海卡");
|
||||
@@ -1124,6 +1213,16 @@ class SmartLoadingServiceTest {
|
||||
value.orderStatusSnapshot(), value.expectedArrivalAt());
|
||||
}
|
||||
|
||||
private static Candidate withArrival(Candidate value, String expectedArrivalAt) {
|
||||
return new Candidate(value.accountId(), value.inboundId(), value.inboundDetailId(), value.orderId(),
|
||||
value.orderNo(), value.inboundCode(), value.warehouseId(), value.channelCode(), value.channelName(),
|
||||
value.destinationWarehouseId(), value.destinationWarehouseCode(), value.destinationWarehouseName(),
|
||||
value.customerName(), value.goodsName(), value.availablePackageCount(), value.unitGrossWeightKg(),
|
||||
value.unitNetWeightKg(), value.unitVolumeCbm(), value.coLoadGroupId(), value.groupNo(),
|
||||
value.memberCount(), value.groupTotalBoxes(), value.locked(), value.blockReason(),
|
||||
value.orderStatusSnapshot(), LocalDateTime.parse(expectedArrivalAt));
|
||||
}
|
||||
|
||||
private static ChannelRow channel(String channelCode, String channelName) {
|
||||
ChannelRow value = new ChannelRow();
|
||||
value.setChannelCode(channelCode); value.setChannelName(channelName);
|
||||
|
||||
@@ -212,6 +212,7 @@ paths:
|
||||
description: >-
|
||||
需要 platform:order:declaration:edit。货物使用订单内 itemNo 定位,不接受货物数据库 ID;
|
||||
不允许修改客户、国内收货仓、订单号、入仓码、状态、仓库/PDA/设备实测、测量审核或计费事实。
|
||||
仅待审核、待测量数据、待分柜阶段允许修改;进入装柜中及后续阶段后拒绝修改。
|
||||
已有测量或计费事实不阻断客户申报快照更新,详情继续以最新申报与既有实测分别展示;
|
||||
只有渠道发生变化时才在写入前校验同柜/分柜/装柜可逆性和重新计价资格,任何校验失败均不写入。
|
||||
正式门禁分别为 /internal/firstleg/v1/inbounds/order-channel-changes/validate 与
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ class OrderDeclarationUpdateContractTest {
|
||||
assertThat(map(row.get("actualVolumeCbm")).get("description").toString()).contains("仅 Firstleg 实测");
|
||||
assertThat(map(row.get("estimatedVolumeCbm")).get("description").toString()).contains("平台运营");
|
||||
assertThat(source).contains("ORDER_DECLARATION_UPDATED_V1", "platform:order:declaration:edit", "A0409",
|
||||
"仅待审核、待测量数据、待分柜阶段允许修改",
|
||||
"/internal/firstleg/v1/inbounds/order-channel-changes/validate",
|
||||
"/internal/firstleg/v1/pricing/order-channel-changes/validate",
|
||||
"/internal/firstleg/v1/pricing/order-channel-changes/apply");
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.FirstlegUnifiedOrderClient;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(OrderDeclarationEditGuard.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=token")
|
||||
class OrderDeclarationEditGuardTest {
|
||||
@MockBean private FirstlegUnifiedOrderClient firstleg;
|
||||
@Resource private OrderDeclarationEditGuard guard;
|
||||
|
||||
@Test
|
||||
void allowsPendingReviewWithoutFirstlegAndWaitSplitWithAuthoritativeSummary() {
|
||||
OrderEntity pendingReview = order(OrderStatus.SUBMITTED);
|
||||
assertThatCode(() -> guard.requireEditable(pendingReview)).doesNotThrowAnyException();
|
||||
|
||||
OrderEntity waitSplit = order(OrderStatus.APPROVED);
|
||||
var request = new FirstlegUnifiedOrderClient.BatchRequest(11L, List.of(21L));
|
||||
var waitSplitSummary = summary("WAIT_SPLIT");
|
||||
when(firstleg.summaries("token", request)).thenReturn(ApiResponse.success(List.of(waitSplitSummary)));
|
||||
assertThatCode(() -> guard.requireEditable(waitSplit)).doesNotThrowAnyException();
|
||||
verify(firstleg).summaries("token", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsLoadingAndLaterStatuses() {
|
||||
OrderEntity order = order(OrderStatus.APPROVED);
|
||||
var request = new FirstlegUnifiedOrderClient.BatchRequest(11L, List.of(21L));
|
||||
for (String status : List.of("LOADING", "LOADED", "DELIVERED")) {
|
||||
var fulfillmentSummary = summary(status);
|
||||
when(firstleg.summaries("token", request)).thenReturn(ApiResponse.success(List.of(fulfillmentSummary)));
|
||||
assertThatThrownBy(() -> guard.requireEditable(order))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已进入装柜或后续阶段");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsClosedWhenFirstlegSummaryIsMissing() {
|
||||
OrderEntity order = order(OrderStatus.APPROVED);
|
||||
var request = new FirstlegUnifiedOrderClient.BatchRequest(11L, List.of(21L));
|
||||
when(firstleg.summaries("token", request)).thenReturn(ApiResponse.success(List.of()));
|
||||
|
||||
assertThatThrownBy(() -> guard.requireEditable(order))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("状态校验服务暂时不可用");
|
||||
}
|
||||
|
||||
private FirstlegUnifiedOrderClient.Summary summary(String mainStatus) {
|
||||
var inbound = mock(FirstlegUnifiedOrderClient.Inbound.class);
|
||||
when(inbound.orderId()).thenReturn(21L);
|
||||
var summary = mock(FirstlegUnifiedOrderClient.Summary.class);
|
||||
when(summary.inbound()).thenReturn(inbound);
|
||||
when(summary.mainStatus()).thenReturn(mainStatus);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private OrderEntity order(OrderStatus status) {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setTenantId(11L); order.setStatus(status);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ class OrderDeclarationUpdateServiceTest {
|
||||
.contains("channelCode", "channelName", "customerRemark", "expectedArrivalAt", "estimatedVolumeCbm",
|
||||
"cargoName", "packageCount", "totalQuantity", "lengthCm", "grossWeightKg");
|
||||
verify(pricingChannelGuard).requireAvailable(100L, 31L, "NEW", "FBA");
|
||||
verify(declarationEditGuard).requireEditable(order);
|
||||
verify(repository).insertHistory(any());
|
||||
verify(repository, never()).insertOutbox(any());
|
||||
assertThat(cargo.getPackageCount()).isEqualTo(20);
|
||||
@@ -129,6 +130,23 @@ class OrderDeclarationUpdateServiceTest {
|
||||
any(), any(), any(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDeclarationUpdateWhenFulfillmentHasReachedLoading() {
|
||||
OrderEntity order = order(OrderStatus.APPROVED, 3);
|
||||
when(repository.findOwned(100L, 200L)).thenReturn(order);
|
||||
org.mockito.Mockito.doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"订单已进入装柜或后续阶段,不能修改客户申报资料"))
|
||||
.when(declarationEditGuard).requireEditable(order);
|
||||
|
||||
assertThatThrownBy(() -> service.update(100L, 200L, 700L, "平台操作员",
|
||||
request(3, "OLD", "旧渠道", "备注")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已进入装柜");
|
||||
|
||||
verify(repository, never()).findCargo(anyLong(), anyLong());
|
||||
verify(repository, never()).updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(),
|
||||
any(), any(), any(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatesPlanningFieldsWithCurrentDeclarationSnapshotAndVersion() {
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED, 3);
|
||||
|
||||
Reference in New Issue
Block a user