diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index 7cad50ad2..d69536545 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -79,7 +79,7 @@ ## 三期装柜与 PDA 资源结算 -- 智能装柜全局工作台由服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选只返回具备实际装柜资格的 `WAIT_SPLIT` 数据,同柜不可拆组必须成员完整、满箱且作为单个分页与分配单元,不能跨页或拆分。分配到柜时继续复核国内中转仓、实际渠道、容量、组锁和版本;推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。 +- 智能装柜全局工作台由服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选状态筛选使用最多四项的 `orderStatuses`,未传表示 `PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING` 全部未完成状态。状态只用于搜索和展示,能否加入由有效柜次占用、剩余箱数、国内中转仓、实际渠道、容量、组锁和版本等事实决定;容积和重量均允许达到柜型额定值的 150%,超过即拒绝。待审核或待测量可先占位,但确认预装和启动 PDA 前仍必须进入 `WAIT_SPLIT`。同柜不可拆组必须成员完整、满箱且作为单个分页与分配单元,不能跨页或拆分。推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。 - PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。 - PDA 部分拣货生成二级子入库单时,数量必须守恒:`已拣数量 + 子单数量 = 拣货前主单可用数量`。子单只允许一层,不能继续拆分。 - `releaseMode=AUTO` 时,释放资源数为 `ceil(实际拣货箱数 × 原资源数 / 原可用箱数)`;`MANUAL` 按 `locationIds` 选择,空数组表示明确释放 0 个。当前契约中未传模式且 `locationIds=null` 时按 AUTO 处理。 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java index 0a5d4090f..cc453bef8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java @@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -22,8 +21,8 @@ public interface PendingReviewOrderClient { @RequestParam(required = false) Long warehouseId, @RequestParam(required = false) Long customerId, @RequestParam(required = false) String inboundCode, - @RequestParam(required = false) LocalDate createdStart, - @RequestParam(required = false) LocalDate createdEnd, + @RequestParam(required = false) String createdStart, + @RequestParam(required = false) String createdEnd, @RequestParam long page, @RequestParam long size); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java index 2be703b5d..39ea213a1 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java @@ -7,10 +7,13 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} public record InboundCandidateQuery( @NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes, @Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode, - LocalDate createdStart, LocalDate createdEnd, CandidateOrderStatus orderStatus, + LocalDate createdStart, LocalDate createdEnd, + @Size(max=4) List<@NotNull CandidateOrderStatus> orderStatuses, @Min(1) long page, @Min(1) @Max(200) long size) { public InboundCandidateQuery { channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes); + orderStatuses = orderStatuses == null ? List.of() + : List.copyOf(new java.util.LinkedHashSet<>(orderStatuses)); if (page == 0) page = 1; if (size == 0) size = 20; } @@ -24,7 +27,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} 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, Boolean canAssign, String blockReason, + String orderStatusSnapshot, LocalDateTime expectedArrivalAt, List groupMembers) { public InboundCandidate { groupMembers = groupMembers == null ? List.of() : List.copyOf(groupMembers); } public Long customerId() { return accountId; } @@ -32,7 +36,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} public record WorkbenchContainerQuery( @NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes, @Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode, - LocalDate createdStart, LocalDate createdEnd, CandidateOrderStatus orderStatus, + LocalDate createdStart, LocalDate createdEnd, @Min(1) long page, @Min(1) @Max(200) long size) { public WorkbenchContainerQuery { channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes); @@ -42,6 +46,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} } public record WorkbenchContainer(Long bookingId, String bookingNo, String cabinetNo, String containerNo, Long warehouseId, String warehouseName, String channelCode, String channelName, String containerType, + BigDecimal capacityCbm, BigDecimal maxWeightKg, 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, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java index 6b6ed1bc2..3a2139bae 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java @@ -106,61 +106,68 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba CASE WHEN current_group.id IS NULL THEN eligible.inboundId END inboundId, MIN(eligible.expectedArrivalAt) sortArrivalAt,MIN(eligible.inboundId) sortInboundId FROM ( - SELECT detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,detail.createdAt, - SUM(detail.availablePackageCount) availablePackageCount - FROM ( - SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode, - o.expected_arrival_at expectedArrivalAt,o.created_at createdAt,i.inbound_detail_id, - SUM(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 - 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 fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id - AND i.inbound_detail_id IS NOT NULL AND i.package_count>0 - WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}) - AND o.channel_code IN - #{channel} - 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')) - GROUP BY o.tenant_id,o.id,o.inbound_code,o.expected_arrival_at,o.created_at,i.inbound_detail_id - HAVING availablePackageCount>0 - ) detail - GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,detail.createdAt + 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 + ON mi.tenant_id=m.tenant_id AND mi.measurement_id=m.id + AND mi.inbound_detail_id IS NOT NULL AND mi.package_count>0 + WHERE 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')),0) + ELSE COALESCE(o.total_package_count,0) END + -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.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),0) availablePackageCount + FROM fl_inbound_order o + WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}) + AND o.channel_code IN + #{channel} + AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT')) + AND ( + o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED', + 'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING') + ) ) eligible LEFT JOIN fl_coload_group_member current_member 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 HAVING (current_group.id IS NULL OR ( - MIN(current_group.group_status)='ACTIVE' AND MAX(current_group.locked_plan_id) IS NULL - AND COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count + COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1) - AND SUM(CASE WHEN eligible.availablePackageCount=current_member.full_package_count THEN 1 ELSE 0 END)=COUNT(*) )) - - AND SUM(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)>0 - - - AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)>0 - - - AND DATE(MIN(eligible.createdAt))>=#{query.createdStart} - AND DATE(MIN(eligible.createdAt))<=#{query.createdEnd} - + + AND MAX(CASE WHEN + + + eligible.candidateStatus='WAIT_MEASURE' + eligible.candidateStatus='WAIT_SPLIT' + eligible.candidateStatus='LOADING' + 1=0 + + + THEN 1 ELSE 0 END)=1 + + + AND MAX(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)=1 + + + AND MAX(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)=1 + + AND MIN(eligible.createdAt)>=#{query.createdStart} + AND MIN(eligible.createdAt)<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY) ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId """) @@ -190,8 +197,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba WHERE member_count.group_id=current_group.id AND member_count.active_flag=1) memberCount, (SELECT SUM(member_boxes.full_package_count) FROM fl_coload_group_member member_boxes WHERE member_boxes.group_id=current_group.id AND member_boxes.active_flag=1) groupTotalBoxes, - CASE WHEN current_group.id IS NULL THEN NULL ELSE FALSE END locked,NULL blockReason, - o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt + CASE WHEN current_group.locked_plan_id IS NOT NULL OR EXISTS ( + SELECT 1 FROM fl_loading_plan_item occupied + WHERE occupied.account_id=o.tenant_id AND occupied.inbound_id=o.id AND occupied.deleted=0 + ) THEN TRUE ELSE FALSE END locked,NULL blockReason, + 'WAIT_SPLIT' 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' @@ -203,7 +213,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba ON current_member.tenant_id=o.tenant_id AND current_member.inbound_id=o.id 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='ACTIVE' - AND current_group.locked_plan_id IS NULL WHERE 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')) @@ -217,7 +226,45 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba ) GROUP BY o.id,m.id,i.inbound_detail_id,current_group.id,current_group.group_no HAVING availablePackageCount>0 - ORDER BY o.inbounded_at ASC,o.id ASC,i.inbound_detail_id ASC + UNION ALL + SELECT o.tenant_id accountId,o.id inboundId,NULL 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,NULL goodsName, + GREATEST(COALESCE(o.total_package_count,0)-COALESCE((SELECT SUM(used.package_count) + FROM fl_loading_plan_item used WHERE used.account_id=o.tenant_id + AND used.inbound_id=o.id AND used.deleted=0),0),0) availablePackageCount, + o.actual_gross_weight_kg/NULLIF(o.total_package_count,0) unitGrossWeightKg, + o.actual_net_weight_kg/NULLIF(o.total_package_count,0) unitNetWeightKg, + o.actual_volume_cbm/NULLIF(o.total_package_count,0) unitVolumeCbm, + current_group.id coLoadGroupId,current_group.group_no groupNo, + (SELECT COUNT(*) FROM fl_coload_group_member member_count + WHERE member_count.group_id=current_group.id AND member_count.active_flag=1) memberCount, + (SELECT SUM(member_boxes.full_package_count) FROM fl_coload_group_member member_boxes + WHERE member_boxes.group_id=current_group.id AND member_boxes.active_flag=1) groupTotalBoxes, + CASE WHEN current_group.locked_plan_id IS NOT NULL OR EXISTS ( + SELECT 1 FROM fl_loading_plan_item occupied + WHERE occupied.account_id=o.tenant_id AND occupied.inbound_id=o.id AND occupied.deleted=0 + ) THEN TRUE ELSE FALSE END locked,NULL blockReason, + CASE WHEN o.process_status='LOADING' THEN 'LOADING' ELSE 'WAIT_MEASURE' END orderStatusSnapshot, + o.expected_arrival_at expectedArrivalAt + FROM fl_inbound_order o + LEFT JOIN fl_coload_group_member current_member + ON current_member.tenant_id=o.tenant_id AND current_member.inbound_id=o.id 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') + WHERE o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED', + 'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','LOADING') + AND ( + + + current_group.id=#{unit.groupId} + (current_group.id IS NULL AND o.tenant_id=#{unit.accountId} AND o.id=#{unit.inboundId}) + + + ) + ORDER BY inboundId ASC,inboundDetailId ASC """) List globalCandidatesByUnits(@Param("units") List units); @@ -226,7 +273,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba