This commit is contained in:
2026-10-20 10:35:52 +08:00
parent ce189f062f
commit c13175bf77
202 changed files with 38397 additions and 7138 deletions
@@ -12,7 +12,7 @@ import java.util.List;
@Mapper
public interface DeliveryOperationMapper extends BaseMapper<DeliveryOperation> {
/** 普通库存流水与原箱回仓凭据共同证明实际退回,每个原箱只计一次箱数。 */
/** 普通流水与原箱凭据证明退回,共享任务按真实扫码及逐票客户归属核验。 */
@Select("SELECT l.id AS ledger_id, di.id AS dispatch_item_id, oi.warehouse_pallet_id AS pallet_id, " +
"l.delta_packages AS packages, l.occurred_at AS received_at FROM ll_delivery_order d " +
"JOIN ll_exception_order e ON e.customer_id=d.customer_id AND e.delivery_order_id=d.id " +
@@ -25,7 +25,7 @@ public interface DeliveryOperationMapper extends BaseMapper<DeliveryOperation> {
"AND di.warehouse_pallet_id=oi.warehouse_pallet_id " +
"AND EXISTS (SELECT 1 FROM ll_dispatch_pda_pick p JOIN ll_dispatch_pda_pick_order a " +
"ON a.customer_id=p.customer_id AND a.pick_id=p.id " +
"JOIN ll_dispatch_pda_task t ON t.customer_id=p.customer_id AND t.id=p.task_id " +
"JOIN ll_dispatch_pda_task t ON t.id=p.task_id " +
"WHERE p.customer_id=d.customer_id AND t.dispatch_order_id=d.dispatch_order_id " +
"AND p.dispatch_item_id=di.id AND p.inventory_position_id=i.inventory_position_id AND a.order_id=e.last_mile_order_id) " +
"WHERE d.customer_id=#{customerId} AND d.id=#{deliveryOrderId} UNION ALL SELECT MIN(r.id),r.source_dispatch_item_id,oi.warehouse_pallet_id,SUM(r.packages),r.received_at " +
@@ -17,7 +17,7 @@ public interface DispatchFeeCargoMapper {
JOIN ll_inventory_balance b ON b.id=p.balance_id AND b.customer_id=di.customer_id
LEFT JOIN ll_order o ON o.id=b.inbound_order_id AND o.customer_id=di.customer_id
LEFT JOIN (SELECT pk.dispatch_item_id,pk.inventory_position_id,SUM(pk.packages) AS packages
FROM ll_dispatch_pda_pick pk JOIN ll_dispatch_pda_task task ON task.id=pk.task_id AND task.customer_id=pk.customer_id
FROM ll_dispatch_pda_pick pk JOIN ll_dispatch_pda_task task ON task.id=pk.task_id
WHERE task.dispatch_order_id=#{dispatchId} AND pk.customer_id=#{customerId}
GROUP BY pk.dispatch_item_id,pk.inventory_position_id) picked
ON picked.dispatch_item_id=di.id AND picked.inventory_position_id=p.id
@@ -45,6 +45,14 @@ public class DeliveryCancellationRepository {
public OutboundOrder lock(String no) { return outbounds.lockGlobalByNo(no); }
/** 独立换标出库保留其原域确认锁。 */
public RelabelApplication lockRelabel(OutboundOrder order) { return relabels.lockByOutbound(order.getCustomerId(),order.getOutboundNo()); }
/** 仅撤销本次出库关联,原生箱和普通仓托均保留客户已确认成果;旧号必须显式清空。 */
public boolean restoreRelabelArrangement(RelabelApplication application,String outboundNo,String fulfillment,LocalDateTime now) {
return relabels.update(null,new LambdaUpdateWrapper<RelabelApplication>()
.eq(RelabelApplication::getId,application.getId()).eq(RelabelApplication::getCustomerId,application.getCustomerId())
.eq(RelabelApplication::getOutboundOrderNo,outboundNo).eq(RelabelApplication::getStatus,"READY_FOR_OUTBOUND")
.set(RelabelApplication::getOutboundOrderNo,null).set(RelabelApplication::getFulfillmentStatus,fulfillment)
.set(RelabelApplication::getUpdatedAt,now))==1;
}
/** 子明细始终按出库客户读取。 */
public List<OutboundPalletItem> items(OutboundOrder order) { return items.findByOutbound(order.getCustomerId(),order.getId()); }
/** 一次取得本出库全部派送锁,不边持来源锁边锁下一张派送。 */
@@ -1,6 +1,5 @@
package com.baiyue.lastleg.delivery.service;
import com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard;
import com.baiyue.lastleg.fleet.trip.FleetDispatchGateway;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -22,9 +21,9 @@ import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
import com.baiyue.lastleg.dispatch.entity.DispatchAggregate;
import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
@@ -93,7 +92,7 @@ import java.util.stream.Collectors;
@Service
public class DeliveryTransferService {
@Resource private DispatchOrderMapper dispatchOrders;
@Resource private DispatchAggregateRepository dispatches;
@Resource private DispatchItemMapper dispatchItems;
@Resource private OutboundOrderMapper outboundOrders;
@Resource private OutboundPalletItemMapper outboundPallets;
@@ -128,7 +127,6 @@ public class DeliveryTransferService {
@Resource private RelabelApplicationService relabelService;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
@Resource private ObjectMapper objectMapper;
@Resource private FleetDispatchBindingGuard fleetBindings;
@Resource private DeliveryExecutionGuard deliveryExecution;
@Resource private com.baiyue.lastleg.dispatch.port.DispatchExecutionPort execution;
@Resource private com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository cancellations;
@@ -912,7 +910,7 @@ public class DeliveryTransferService {
|| dispatch.stage()!=com.baiyue.lastleg.dispatch.port.DispatchTypes.Stage.BEFORE_LOADING
|| dispatch.counts()==null || dispatch.counts().pickedPackageCount()>0 || dispatch.counts().shippedPackageCount()>0)
throw conflict("排车已开始执行,不能直接取消出库");
fleetBindings.requireUnbound(dispatch.dispatchId());
// 唯一Fleet资源记录随整车自动创建,不能把本车正常关系当作额外取消阻断。
}
if (!Set.of("WAIT_OUTBOUND", "WAIT_PICK").contains(outbound.getStatus())) {
throw conflict("Only an unexecuted outbound order can be cancelled");
@@ -923,6 +921,8 @@ public class DeliveryTransferService {
|| !Set.of("WAIT_PICK", "WAIT_OUTBOUND").contains(i.getStatus()))) {
throw conflict("Outbound order cannot be cancelled after execution has started");
}
boolean relabelArrangement = "RELABEL_OUTBOUND".equals(outbound.getOutboundType());
String pendingRelabelFulfillment = null;
List<DeliveryOrder> existingDeliveries = cancellations.deliveries(outbound);
if (existingDeliveries.stream().anyMatch(delivery -> !"WAIT_DELIVERY".equals(delivery.getStatus())))
throw conflict("Outbound order cannot be cancelled while delivery is in progress");
@@ -932,6 +932,7 @@ public class DeliveryTransferService {
if ("RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
if (application == null || !customerId.equals(application.getCustomerId())
|| !outbound.getOutboundNo().equals(application.getOutboundOrderNo())
|| !Objects.equals(outbound.getOverseasWarehouseId(),application.getOverseasWarehouseId())
|| !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus()))
throw conflict("换标出库关联状态已变化,不能取消");
for (OutboundPalletItem item : outboundItems) {
@@ -939,6 +940,7 @@ public class DeliveryTransferService {
for (String mark : Objects.toString(item.getCartonMarks(), "").split(",")) {
ReturnInventoryCarton carton = cancellations.lockCarton(mark);
if (carton == null || !customerId.equals(carton.getCustomerId())
|| !Objects.equals(outbound.getOverseasWarehouseId(),carton.getOverseasWarehouseId())
|| !Objects.equals(application.getId(), carton.getRelabelApplicationId())
|| !"OUTBOUND_PENDING".equals(carton.getStatus()))
throw conflict("退货换标箱预留状态异常,不能取消出库");
@@ -946,8 +948,10 @@ public class DeliveryTransferService {
cancellations.save(carton);
}
}
relabelService.changeStatus(customerId, application.getRelabelNo(), operatorId,
new RelabelRequests.ChangeStatus("CANCELLED", request.reason().trim()));
// 原生箱和普通仓托均只撤销本次安排;整车分支仍须先通过父锁、阶段和零扫码门禁。
pendingRelabelFulfillment = "TRUCK".equals(application.getDeliveryMethod()) ? "WAIT_PRE_DISPATCH" : "WAIT_OUTBOUND_CREATION";
if (!cancellations.restoreRelabelArrangement(application,outbound.getOutboundNo(),pendingRelabelFulfillment,now))
throw conflict("原换标出库关联已变化,未取消本次安排");
}
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) {
for (String mark : outboundItems.stream().map(OutboundPalletItem::getCartonMarks).sorted().toList()) {
@@ -968,6 +972,13 @@ public class DeliveryTransferService {
var current=cancellations.lockSource(customerId,selected.getId());
if(current!=null && outboundNo.equals(current.getOutboundOrderNo()))cancelSources.put(current.getId(),current);
}
if (relabelArrangement) {
// 仅清本次出库/派送关联,来源主状态和换标完成事实保持原值,不能退回未换标库存。
for (var order : cancelSources.values()) {
if (!cancellations.restore(order,outboundNo,order.getCurrentStatus(),"RELABEL:"+pendingRelabelFulfillment,now))
throw conflict("原换标来源出库关联已变化,请刷新核对");
}
}
for (DeliveryOrder delivery : existingDeliveries) {
int voidRequests = 0;
for (var fee : feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId())) {
@@ -1066,7 +1077,7 @@ public class DeliveryTransferService {
OutboundOrder outbound = outboundOrders.findGlobalByNo(code);
if (outbound != null && boundWarehouseId.equals(outbound.getOverseasWarehouseId())) {
if ("DISPATCH_DELIVERY".equals(outbound.getOutboundType()) && StringUtils.hasText(outbound.getDispatchNo())) {
DispatchOrder dispatch = dispatchOrders.findGlobalByDispatchNo(outbound.getDispatchNo());
DispatchAggregate dispatch = dispatches.find(outbound.getDispatchNo());
if (dispatch != null && boundWarehouseId.equals(dispatch.getOverseasWarehouseId())) {
return new DeliveryResponses.PdaTaskResolution("DISPATCH", null, outbound.getOutboundNo(),
dispatch.getDispatchNo());
@@ -1076,7 +1087,7 @@ public class DeliveryTransferService {
if (task != null) return outboundResolution(outbound, task);
}
DispatchOrder dispatch = dispatchOrders.findGlobalByDispatchNo(code);
DispatchAggregate dispatch = dispatches.find(code);
if (dispatch != null && boundWarehouseId.equals(dispatch.getOverseasWarehouseId())) {
return new DeliveryResponses.PdaTaskResolution("DISPATCH", null, null, dispatch.getDispatchNo());
}
@@ -1,57 +0,0 @@
package com.baiyue.lastleg.delivery.service;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository;
import com.baiyue.lastleg.fleet.trip.FleetDispatchGateway;
import org.springframework.stereotype.Service;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import java.util.*;
/** 外接绑定和约定运费共同消费同一份逐来源货物。 */
@Service
public class DispatchCargoSourceQuery {
private final DispatchCargoSourceRepository repository;
/** 装配当前库存与本次拣货的真实来源入口。 */
public DispatchCargoSourceQuery(DispatchCargoSourceRepository repository) { this.repository = repository; }
/** 新增普通派送应收只绑定真实未约定来源;混合多票时保留派送归属。 */
public Set<Long> ordinaryOrderIds(DispatchOrder dispatch, Set<String> externalInboundCodes) {
return repository.sources(dispatch.getCustomerId(), dispatch.getId()).stream()
.filter(row -> !externalInboundCodes.contains(row.inboundCode()))
.map(row -> row.orderId()).filter(Objects::nonNull).collect(java.util.stream.Collectors.toSet());
}
/** 每条普通排车明细核平实际组成;箱级换标和退货保留其专用来源快照。 */
public List<FleetDispatchGateway.SourceCargo> sources(DispatchOrder dispatch, List<DispatchItem> items) {
if (items.isEmpty()) return List.of();
var rows = repository.sources(dispatch.getCustomerId(), dispatch.getId());
var result = new ArrayList<FleetDispatchGateway.SourceCargo>();
for (var item : items) {
String destination = FleetDispatchGateway.destinationKey(item.getDestinationType(),
item.getDestinationWarehouseCode(), item.getFbaWarehouseCode(), item.getDeliveryAddress(), item.getPostalCode());
// 实收重派按不可变实收记录选货,箱级换标按箱来源选货;都不是普通整托候选。
if (item.getReturnReceiptId() != null || Set.of("RELABEL", "RETURN_RELABEL").contains(Objects.toString(item.getSourceStage(), ""))) {
result.add(new FleetDispatchGateway.SourceCargo(item.getInboundCode(),
item.getWarehousePalletId() == null ? null : item.getWarehousePalletId().toString(),
item.getPalletNo(), Objects.requireNonNullElse(item.getPackageCount(), 0), destination));
continue;
}
var selected = rows.stream().filter(row -> Objects.equals(row.dispatchItemId(), item.getId())).toList();
if (selected.isEmpty() || item.getPackageCount() == null || selected.stream().mapToLong(row -> row.packages()).sum() != item.getPackageCount())
throw conflict("实际货物组成与排车箱数不一致,请核对收货、托盘及本次拣货来源");
for (var row : selected) {
if (row.inboundCode() == null || row.inboundCode().isBlank() || row.packages() <= 0
|| !Objects.equals(row.warehousePalletId(), item.getWarehousePalletId()))
throw conflict("货物缺少真实入仓来源或实物托盘,不能识别为普通货跳过外接核对");
result.add(new FleetDispatchGateway.SourceCargo(row.inboundCode(), row.warehousePalletId().toString(),
row.palletNo(), Math.toIntExact(row.packages()), destination));
}
}
return List.copyOf(result);
}
/** 来源不足直接阻断,不回退展示字段。 */
private static ResponseStatusException conflict(String reason) { return new ResponseStatusException(HttpStatus.CONFLICT, reason); }
}