This commit is contained in:
2026-09-03 21:48:40 +08:00
parent 2b461cc6e8
commit eca2c94aa3
7 changed files with 222 additions and 49 deletions
@@ -87,6 +87,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
FROM (
SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode,
o.expected_arrival_at expectedArrivalAt,o.created_at createdAt,
CASE WHEN o.process_status='WAIT_SPLIT' THEN 'WAIT_SPLIT'
WHEN o.process_status='LOADING' THEN 'LOADING' ELSE 'WAIT_MEASURE' END candidateStatus,
CASE WHEN o.process_status='WAIT_SPLIT' THEN
COALESCE((SELECT SUM(mi.package_count) FROM fl_inbound_measurement m
JOIN fl_inbound_measurement_item mi
@@ -132,7 +134,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
AND current_member.active_flag=1
LEFT JOIN fl_coload_group current_group
ON current_group.id=current_member.group_id AND current_group.group_status IN ('ACTIVE','LOCKED')
ON current_group.id=current_member.group_id AND current_group.group_status IN ('ACTIVE','LOCKED')
WHERE eligible.candidateStatus<>'WAIT_SPLIT' OR eligible.availablePackageCount>0
GROUP BY current_group.id,
CASE WHEN current_group.id IS NULL THEN eligible.accountId END,
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END
@@ -41,7 +41,7 @@ import java.util.*;
/** 智能装柜的自动计划、候选、推荐、人工分配和删除编排。 */
@Service
public class SmartLoadingService {
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.10");
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.50");
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"),
@@ -278,7 +278,8 @@ public class SmartLoadingService {
.sorted(Comparator.comparing(InboundCandidate::inboundId)).toList();
InboundCandidate first = ordered.get(0);
int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum();
if (packages != metadata.groupTotalBoxes()) return null;
boolean occupied = ordered.stream().anyMatch(candidate -> Boolean.TRUE.equals(candidate.locked()));
if (!occupied && packages != metadata.groupTotalBoxes()) return null;
boolean hasVolume = ordered.stream().anyMatch(candidate -> candidate.availableVolumeCbm() != null);
BigDecimal volume = hasVolume ? ordered.stream().map(InboundCandidate::availableVolumeCbm)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP) : null;
@@ -299,7 +300,7 @@ public class SmartLoadingService {
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(),
metadata.memberCount(), metadata.groupTotalBoxes(),
ordered.stream().anyMatch(candidate -> Boolean.TRUE.equals(candidate.locked())),
occupied,
ordered.stream().allMatch(candidate -> Boolean.TRUE.equals(candidate.canAssign())),
ordered.stream().map(InboundCandidate::blockReason).filter(StringUtils::hasText).findFirst().orElse(null),
groupStatus(ordered),
@@ -1017,7 +1018,7 @@ public class SmartLoadingService {
result.put(member.inboundId(), details);
}
if (!groupFits(booking, planItems, group, available)) {
throw validation("同柜不可拆组整体超过货柜容积或重量的110%上限");
throw validation("同柜不可拆组整体超过货柜容积或重量的150%上限");
}
return result;
}
@@ -1270,7 +1271,7 @@ public class SmartLoadingService {
if (b.getCapacityCbm()==null || b.getMaxWeightKg()==null) throw validation("货柜容量和最大承重未配置");
if (v.compareTo(b.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO))>0
|| w.compareTo(b.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO))>0) {
throw validation("超过货柜容积或重量的110%上限");
throw validation("超过货柜容积或重量的150%上限");
}
}
private void checkPlanCapacity(ContainerBookingEntity booking, List<LoadingPlanItemEntity> items) {
@@ -1280,12 +1281,12 @@ public class SmartLoadingService {
BigDecimal weight = items.stream().map(x -> unit(x.getGrossWeightKg()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (weight.compareTo(booking.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO)) > 0) {
throw validation("超过货柜重量的110%上限");
throw validation("超过货柜重量的150%上限");
}
BigDecimal volume = items.stream().map(x -> unit(x.getVolumeCbm()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (volume.compareTo(booking.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO)) > 0) {
throw validation("超过货柜容积的110%上限");
throw validation("超过货柜容积的150%上限");
}
}
private int fittingBoxes(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c){
@@ -486,13 +486,16 @@ paths:
summary: 全局分页查询智能装柜候选
description: >-
需要 platform:firstleg:smart-loading:list 权限。查询不绑定柜次;渠道、仓库、客户、入仓码、
预计到货日期和订单状态共同作用于全局候选。每个普通入库单返回一行;同柜不可拆组作为一个
订单创建日期和订单状态共同作用于全局候选。每个普通入库单返回一行;同柜不可拆组作为一个
分页单元返回,groupMembers 包含完整成员,不会跨页或因单个成员命中搜索条件而拆散。
平均单箱毛重、净重按整票实测重量除以总箱数,平均单箱体积按剩余规格数量加权。
WAIT_SPLIT 返回 Firstleg 可用箱数;PENDING_REVIEW 返回 Order 权威待审核订单并允许先加入装柜清单。
订单审核状态只用于筛选和展示,不直接决定能否占位;已被有效柜次占用、LOADED、COMPLETED、
已锁定或成员不完整的组永不返回。分配时仍由目标 planId 接口复核真实渠道、仓库、容量、
组锁和并发版本;确认预装/PDA 前必须完成审核与测量并进入 WAIT_SPLIT。
orderStatuses 可重复传参且最多四项;未传或空数组表示 PENDING_REVIEW、WAIT_MEASURE、WAIT_SPLIT、
LOADING 全部未完成状态,已完成装柜永不返回。状态映射为:Order SUBMITTED=PENDING_REVIEW;
Firstleg WAIT_MEASURE/MEASURING/MEASURE_PENDING/MEASURE_REJECTED/WAIT_PRICING/FEE_CONFIRMING/INBOUNDING
=WAIT_MEASURE;WAIT_SPLIT=WAIT_SPLIT;LOADING=LOADING。状态只用于搜索和展示,canAssign 由有效柜次占用、
剩余箱数、同柜组、仓库、渠道和容量等事实决定;容积和重量均允许达到柜型额定值的150%,超过即拒绝。
待审核及待测量可以先加入清单;确认预装/PDA 前
仍必须完成审核与测量并进入 WAIT_SPLIT。同柜不可拆组仅在全部成员命中当前状态集合时作为一个分页单位返回。
parameters:
- name: channelGroupCodes
in: query
@@ -514,7 +517,15 @@ paths:
- {name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 40}}
- {name: createdStart, in: query, required: false, schema: {type: string, format: date}}
- {name: createdEnd, 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: orderStatuses
in: query
required: false
description: 可重复传参,未传或空数组表示全部四种未完成状态
schema:
type: array
maxItems: 4
uniqueItems: true
items: {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:
@@ -525,8 +536,8 @@ paths:
get:
summary: 分页查询智能装柜工作台柜次
description: >-
需要 platform:firstleg:smart-loading:list 权限。与全局候选使用相同的渠道、仓库、客户、入仓码、
订单创建日期及订单状态筛选;客户及入仓事实以柜内任一有效装柜明细命中为准,查询不会修改或拆散同柜组。
需要 platform:firstleg:smart-loading:list 权限。柜次查询只使用渠道、仓库、客户、入仓码及订单创建日期;
候选的 orderStatuses 不作用于柜次列表。客户及入仓事实以柜内任一有效装柜明细命中为准。
只返回 WAIT_LOADING、EDITING 或 LOADING 的 ACTIVE 柜次,COMPLETED 永不返回。
parameters:
- name: channelGroupCodes
@@ -543,7 +554,6 @@ paths:
- {name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 40}}
- {name: createdStart, in: query, required: false, schema: {type: string, format: date}}
- {name: createdEnd, 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:
@@ -605,7 +615,8 @@ paths:
summary: 按入仓码分配计划装柜箱数
description: >-
需要 platform:firstleg:smart-loading:assign 权限。操作员只选择某个入仓码的本次计划箱数;
服务端在单一事务内按剩余规格箱数比例生成内部规格快照,用于体积估算及兼容 PDA/拆单。
服务端在单一事务内按剩余规格箱数比例生成内部规格快照,用于体积估算及兼容 PDA/拆单;
柜内总容积和总重量分别允许达到柜型额定值的150%,超过即拒绝。
只记录数量计划,具体实装箱号归属为 UNVERIFIED,不得将内部比例分摊宣称为现场箱号事实。
parameters:
- {name: planId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
@@ -2007,7 +2018,7 @@ components:
type: object
required: [accountId, customerId, inboundId, orderId, orderNo, inboundCode, warehouseId, channelCode,
channelName, customerName, goodsSummary, availablePackageCount, unitGrossWeightKg,
unitNetWeightKg, averageUnitVolumeCbm, availableVolumeCbm, locked, groupMembers]
unitNetWeightKg, averageUnitVolumeCbm, availableVolumeCbm, locked, canAssign, groupMembers]
properties:
accountId: {type: string, pattern: '^[1-9][0-9]*$'}
customerId: {type: string, pattern: '^[1-9][0-9]*$', description: 与 accountId 相同的客户技术关联ID}
@@ -2015,7 +2026,7 @@ components:
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
orderNo: {type: string}
inboundCode: {type: string}
orderStatusSnapshot: {type: string, nullable: true}
orderStatusSnapshot: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING]}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
channelCode: {type: string}
@@ -2025,7 +2036,7 @@ components:
destinationWarehouseName: {type: string, nullable: true}
customerName: {type: string}
goodsSummary: {type: string, nullable: true}
availablePackageCount: {type: integer, minimum: 1, description: 普通行剩余箱数;组行完整成员箱数合计}
availablePackageCount: {type: integer, minimum: 0, description: 普通行剩余箱数;组行完整成员箱数合计}
unitGrossWeightKg: {type: number, minimum: 0, nullable: true}
unitNetWeightKg: {type: number, minimum: 0, nullable: true}
averageUnitVolumeCbm: {type: number, minimum: 0, nullable: true}
@@ -2035,6 +2046,7 @@ components:
memberCount: {type: integer, minimum: 2, nullable: true}
groupTotalBoxes: {type: integer, minimum: 1, nullable: true}
locked: {type: boolean}
canAssign: {type: boolean, description: 当前是否可加入柜次;与搜索状态解耦,以占用及业务约束事实计算}
blockReason: {type: string, nullable: true}
groupMembers:
type: array
@@ -2049,7 +2061,7 @@ components:
orderNo: {type: string}
inboundCode: {type: string}
customerName: {type: string}
availablePackageCount: {type: integer, minimum: 1}
availablePackageCount: {type: integer, minimum: 0}
orderStatusSnapshot: {type: string, nullable: true}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
SmartLoadingWorkbenchContainerPageEnvelope: