2026-08-19 16:24:02 +08:00
package com.baiyue.lastleg.service ;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper ;
2026-08-26 11:05:26 +08:00
import com.baomidou.mybatisplus.core.toolkit.IdWorker ;
2026-08-19 16:24:02 +08:00
import com.baomidou.mybatisplus.extension.plugins.pagination.Page ;
import com.baiyue.common.core.api.PageResult ;
2026-08-31 12:13:20 +08:00
import com.baiyue.common.event.ContainerNumbers ;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse ;
2026-09-07 23:58:18 +08:00
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos ;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.dto.LastlegOrderRequests ;
import com.baiyue.lastleg.dto.LastlegOrderSummary ;
2026-08-28 13:54:32 +08:00
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail ;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary ;
2026-09-15 16:10:55 +08:00
import com.baiyue.lastleg.dto.ContainerArrivalTimeView ;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.entity.LastlegOrder ;
import com.baiyue.lastleg.entity.LastlegOrderDetail ;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory ;
2026-08-26 11:05:26 +08:00
import com.baiyue.lastleg.entity.LastlegOrderOperationLog ;
2026-09-07 23:58:18 +08:00
import com.baiyue.lastleg.entity.LastlegOrderManualNode ;
2026-09-07 15:07:34 +08:00
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation ;
2026-09-15 16:10:55 +08:00
import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation ;
2026-09-07 15:07:34 +08:00
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway ;
2026-08-24 15:38:28 +08:00
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton ;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper ;
2026-08-28 14:20:48 +08:00
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService ;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper ;
import com.baiyue.lastleg.mapper.LastlegOrderMapper ;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper ;
2026-09-09 16:37:28 +08:00
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos ;
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource ;
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService ;
2026-08-26 11:05:26 +08:00
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper ;
2026-09-07 23:58:18 +08:00
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper ;
2026-09-07 15:07:34 +08:00
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper ;
2026-09-15 16:10:55 +08:00
import com.baiyue.lastleg.mapper.ContainerArrivalTimeOperationMapper ;
2026-08-22 12:35:32 +08:00
import com.baiyue.lastleg.model.LastlegOrderType ;
2026-09-18 11:41:04 +08:00
import com.baiyue.lastleg.model.LastlegOrderStatus ;
2026-09-11 16:17:16 +08:00
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate ;
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper ;
2026-08-19 16:24:02 +08:00
import jakarta.annotation.Resource ;
import org.springframework.http.HttpStatus ;
import org.springframework.stereotype.Service ;
import org.springframework.transaction.annotation.Transactional ;
import org.springframework.util.StringUtils ;
import org.springframework.web.server.ResponseStatusException ;
import java.nio.charset.StandardCharsets ;
2026-08-31 14:39:40 +08:00
import java.io.ByteArrayOutputStream ;
import java.io.IOException ;
import java.io.OutputStream ;
2026-09-18 14:58:40 +08:00
import java.math.BigDecimal ;
2026-08-19 16:24:02 +08:00
import java.time.LocalDateTime ;
2026-09-07 15:07:34 +08:00
import java.security.MessageDigest ;
2026-08-19 16:24:02 +08:00
import java.util.ArrayList ;
2026-08-28 16:30:02 +08:00
import java.util.LinkedHashMap ;
2026-09-15 16:10:55 +08:00
import java.util.LinkedHashSet ;
2026-08-19 16:24:02 +08:00
import java.util.List ;
2026-08-28 16:30:02 +08:00
import java.util.Map ;
2026-08-19 16:24:02 +08:00
import java.util.Objects ;
2026-09-14 20:21:07 +08:00
import java.util.Set ;
2026-09-14 09:58:30 +08:00
import java.util.stream.Collectors ;
2026-08-19 16:24:02 +08:00
@Service
public class LastlegOrderService {
public static final String PENDING_ASSIGNMENT = "PENDING_ASSIGNMENT" ;
public static final String DESTINATION_ASSIGNED = "DESTINATION_ASSIGNED" ;
public static final String INBOUND_PLANNED = "INBOUND_PLANNED" ;
2026-08-28 14:20:48 +08:00
public static final String PALLET_MERGE_REQUIRED = "REQUIRED" ;
public static final String PALLET_MERGED = "MERGED" ;
2026-09-07 23:58:18 +08:00
private static final List < String > MANUAL_NODE_CODES = List . of (
"DOMESTIC_CUSTOMS_COMPLETED" , "ESTIMATED_DEPARTURE" , "ACTUAL_DEPARTURE" , "ESTIMATED_ARRIVAL" ,
"OVERSEAS_CUSTOMS_COMPLETED" , "ACTUAL_ARRIVAL_WAITING_UNLOAD" , "PICKUP_APPOINTED" , "UNLOADED" );
2026-08-19 16:24:02 +08:00
@Resource private LastlegOrderMapper orderMapper ;
2026-10-09 15:20:15 +08:00
@Resource private com . baiyue . lastleg . returnreceipt . service . ReturnReceiptService returnReceipts ;
2026-08-19 16:24:02 +08:00
@Resource private LastlegOrderDetailMapper detailMapper ;
@Resource private LastlegOrderStatusHistoryMapper historyMapper ;
2026-09-09 16:37:28 +08:00
@Resource private OverseasFeeSourceService feeSources ;
2026-08-26 11:05:26 +08:00
@Resource private LastlegOrderOperationLogMapper operationLogMapper ;
2026-09-07 23:58:18 +08:00
@Resource private LastlegOrderManualNodeMapper manualNodeMapper ;
2026-08-24 15:38:28 +08:00
@Resource private InboundPlanCartonMapper cartonMapper ;
2026-08-28 13:54:32 +08:00
@Resource private FbaWarehouseValidator fbaWarehouseValidator ;
2026-08-28 14:20:48 +08:00
@Resource private InventoryCommandService inventory ;
2026-09-07 15:07:34 +08:00
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations ;
2026-09-15 16:10:55 +08:00
@Resource private ContainerArrivalTimeOperationMapper containerArrivalTimeOperations ;
2026-09-07 15:07:34 +08:00
@Resource private WarehouseResourceGateway warehouseResourceGateway ;
2026-09-11 16:17:16 +08:00
@Resource private ContainerShippingGateMapper containerShippingGates ;
2026-10-09 11:49:17 +08:00
@Resource private ReturnOrderReferenceResolver returnOrderReferences ;
2026-08-19 16:24:02 +08:00
public PageResult < LastlegOrderSummary > page ( String customerId , LastlegOrderRequests . Query query ) {
2026-09-14 09:58:30 +08:00
return page ( customerId , query , null );
}
public PageResult < LastlegOrderSummary > page ( String customerId , LastlegOrderRequests . Query query , String relabelNo ) {
2026-08-19 16:24:02 +08:00
Page < LastlegOrder > page = orderMapper . selectPage (
2026-10-08 15:31:56 +08:00
new Page <> ( query . page (), query . size ()), filter ( customerId , query , relabelNo ). orderByDesc ( LastlegOrder :: getCreatedAt ). orderByDesc ( LastlegOrder :: getId ));
2026-08-28 16:30:02 +08:00
Map < Long , List < LastlegOrderMapper . PalletMergeRelationRow >> mergeRelations = palletMergeRelations ( page . getRecords ());
2026-10-09 11:49:17 +08:00
returnOrderReferences . populateForRead ( page . getRecords ());
refreshReturnVolumes ( page . getRecords ());
2026-09-11 16:17:16 +08:00
Map < String , String > containerNodes = containerNodes ( page . getRecords ());
2026-09-14 09:58:30 +08:00
Map < Long , String > dispatchStatuses = dispatchStatuses ( page . getRecords ());
2026-09-18 14:58:40 +08:00
Map < Long , LastlegOrderMapper . OrderWeightRow > weights = orderWeights ( page . getRecords ());
2026-09-22 08:49:21 +08:00
Map < Long , List < com . baiyue . lastleg . dto . RelabelSubOrder >> relabels = relabelSubOrders ( page . getRecords ());
2026-10-09 16:12:02 +08:00
Map < Long , LastlegOrderSummary . BusinessSource > businessSources = businessSources ( page . getRecords ());
2026-09-14 09:58:30 +08:00
return new PageResult <> ( page . getRecords (). stream (). map ( order -> summary ( order , mergeRelations . get ( order . getId ()),
2026-09-11 16:17:16 +08:00
StringUtils . hasText ( order . getSourceContainerBookingId ())
2026-09-18 14:58:40 +08:00
? containerNodes . get ( order . getSourceContainerBookingId ()) : null ,
2026-09-22 08:49:21 +08:00
dispatchStatuses . getOrDefault ( order . getId (), "UNSCHEDULED" ), weights . get ( order . getId ()),
2026-10-09 16:12:02 +08:00
relabels . getOrDefault ( order . getId (), List . of ()), businessSources . get ( order . getId ()))). toList (),
2026-08-19 16:24:02 +08:00
page . getCurrent (), page . getSize (), page . getTotal (), page . getPages ());
}
2026-08-27 17:16:33 +08:00
@Transactional ( readOnly = true )
2026-08-28 13:54:32 +08:00
public PageResult < ContainerPlanProgressSummary > containerProgressPage (
LastlegOrderRequests . ContainerProgressQuery query ) {
2026-08-27 17:16:33 +08:00
if ( query . createdFrom () != null && query . createdTo () != null
&& query . createdFrom (). isAfter ( query . createdTo ())) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST ,
"createdFrom must not be after createdTo" );
}
2026-08-28 13:54:32 +08:00
long total = orderMapper . countContainerProgress ( query );
List < ContainerPlanProgressSummary > records = total == 0 ? List . of ()
2026-08-31 12:13:20 +08:00
: orderMapper . pageContainerProgress ( query , ( query . page () - 1 ) * query . size (), query . size ())
2026-09-07 16:36:21 +08:00
. stream (). map ( this :: containerSummaryForRead ). toList ();
2026-08-27 17:16:33 +08:00
long pages = total == 0 ? 0 : ( total + query . size () - 1 ) / query . size ();
return new PageResult <> ( records , query . page (), query . size (), total , pages );
}
2026-08-28 13:54:32 +08:00
@Transactional ( readOnly = true )
public ContainerPlanProgressDetail containerProgressDetail ( String sourceContainerBookingId ) {
String bookingId = sourceContainerBookingId == null ? null : sourceContainerBookingId . trim ();
if ( ! StringUtils . hasText ( bookingId )) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "来源柜预订ID不能为空" );
}
ContainerPlanProgressSummary summary = orderMapper . findContainerProgress ( bookingId );
if ( summary == null ) {
throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "来源柜不存在" );
}
2026-09-07 16:36:21 +08:00
return new ContainerPlanProgressDetail ( containerSummaryForRead ( summary ),
2026-08-31 12:13:20 +08:00
orderMapper . findContainerProgressItems ( bookingId ));
}
2026-09-15 16:10:55 +08:00
@Transactional ( readOnly = true )
public List < ContainerArrivalTimeView > containerArrivalTimes (
LastlegOrderRequests . ContainerArrivalTimeQuery request ) {
LinkedHashSet < String > bookingIds = new LinkedHashSet <> ();
for ( String value : request . sourceContainerBookingIds ()) bookingIds . add ( normalizeBookingId ( value ));
if ( bookingIds . size () != request . sourceContainerBookingIds (). size ()) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "来源柜预订ID不能重复" );
}
return orderMapper . findContainerArrivalTimes ( List . copyOf ( bookingIds ));
}
@Transactional
public ContainerArrivalTimeView changeExpectedWarehouseArrival ( String sourceContainerBookingId ,
String idempotencyKey , LastlegOrderRequests . ChangeExpectedWarehouseArrival request ,
String operatorName ) {
String bookingId = normalizeBookingId ( sourceContainerBookingId );
String key = normalizeIdempotencyKey ( idempotencyKey );
String requestHash = hash ( bookingId + "\n" + request . estimatedWarehouseArrivalAt () + "\n"
+ request . expectedVersion () + "\n" + Objects . toString ( request . remark (), "" ));
ContainerArrivalTimeOperation replay = containerArrivalTimeOperations . findByKey ( key );
if ( replay != null ) {
if ( ! bookingId . equals ( replay . getSourceContainerBookingId ())
|| ! requestHash . equals ( replay . getRequestHash ())) {
throw new ResponseStatusException ( HttpStatus . CONFLICT ,
"Idempotency-Key 已用于其他预计到仓时间修改请求" );
}
return requireContainerArrivalTime ( bookingId );
}
List < LastlegOrder > orders = orderMapper . lockContainerOrders ( bookingId );
if ( orders . isEmpty ()) throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "来源柜不存在" );
int version = requireSingleArrivalTimeVersion ( orders );
if ( version != request . expectedVersion ()) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "预计到仓时间版本已变化,请刷新后重试" );
}
if ( orderMapper . countContainerPlans ( bookingId ) > 0 ) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "该柜已生成入库计划,预计到仓时间不可修改" );
}
int updated = orderMapper . updateContainerExpectedWarehouseArrival ( bookingId ,
request . estimatedWarehouseArrivalAt (), version );
if ( updated != orders . size ()) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "柜内预计到仓时间发生并发变化" );
}
ContainerArrivalTimeOperation operation = new ContainerArrivalTimeOperation ();
operation . setId ( IdWorker . getId ()); operation . setSourceContainerBookingId ( bookingId );
operation . setIdempotencyKey ( key ); operation . setRequestHash ( requestHash );
operation . setResultVersion ( version + 1 ); operation . setOperatorName ( operatorName );
operation . setRemark ( request . remark () == null ? null : request . remark (). trim ());
operation . setCreatedAt ( LocalDateTime . now ()); containerArrivalTimeOperations . insert ( operation );
return requireContainerArrivalTime ( bookingId );
}
private ContainerArrivalTimeView requireContainerArrivalTime ( String bookingId ) {
List < ContainerArrivalTimeView > values = orderMapper . findContainerArrivalTimes ( List . of ( bookingId ));
if ( values . size () != 1 ) throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "来源柜不存在" );
if ( values . get ( 0 ). version () < 0 ) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "柜内预计到仓时间版本不一致" );
}
return values . get ( 0 );
}
private int requireSingleArrivalTimeVersion ( List < LastlegOrder > orders ) {
Set < Integer > versions = orders . stream (). map ( LastlegOrder :: getArrivalTimeVersion )
. map ( value -> value == null ? 0 : value ). collect ( Collectors . toSet ());
if ( versions . size () != 1 ) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "柜内预计到仓时间版本不一致" );
}
return versions . iterator (). next ();
}
2026-09-07 15:07:34 +08:00
@Transactional
public ContainerPlanProgressSummary changeReceivingWarehouse ( String sourceContainerBookingId ,
String idempotencyKey , LastlegOrderRequests . ChangeReceivingWarehouse request , String operatorName ) {
String bookingId = normalizeBookingId ( sourceContainerBookingId );
String key = normalizeIdempotencyKey ( idempotencyKey );
String requestHash = hash ( bookingId + "\n" + request . overseasWarehouseId () + "\n"
+ request . expectedVersion () + "\n" + Objects . toString ( request . remark (), "" ));
ReceivingWarehouseOperation replay = receivingWarehouseOperations . findByKey ( key );
if ( replay != null ) {
if ( ! bookingId . equals ( replay . getSourceContainerBookingId ()) || ! requestHash . equals ( replay . getRequestHash ())) {
throw new ResponseStatusException ( HttpStatus . CONFLICT ,
"Idempotency-Key 已用于其他接柜仓修改请求" );
}
return requireFrozenContainer ( orderMapper . findContainerProgress ( bookingId ));
}
List < LastlegOrder > orders = orderMapper . lockContainerOrders ( bookingId );
if ( orders . isEmpty ()) throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "来源柜不存在" );
int version = requireSingleAssignmentVersion ( orders );
if ( version != request . expectedVersion ()) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "接柜仓分配版本已变化,请刷新后重试" );
}
if ( orderMapper . countReceivingSessions ( bookingId ) > 0 ) {
throw new ResponseStatusException ( HttpStatus . CONFLICT ,
"海外PDA已建立接柜会话,接柜仓已永久锁定" );
}
WarehouseResourceGateway . WarehouseReference warehouse =
warehouseResourceGateway . requireActiveSharedWarehouse ( request . overseasWarehouseId ());
2026-09-08 16:16:20 +08:00
for ( LastlegOrder order : orders ) {
inventory . rebindReceivingWarehouse ( order . getCustomerId (), order . getId (), warehouse . warehouseId (),
warehouse . warehouseCode (), warehouse . warehouseName (), operatorName , key );
}
2026-09-07 15:07:34 +08:00
int updated = orderMapper . updateContainerReceivingWarehouse ( bookingId ,
Long . toString ( warehouse . warehouseId ()), warehouse . warehouseCode (), warehouse . warehouseName (), version );
if ( updated != orders . size ()) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "柜内接柜仓分配发生并发变化" );
}
orderMapper . updateContainerPlanReceivingWarehouse ( bookingId , warehouse . warehouseId ());
ReceivingWarehouseOperation operation = new ReceivingWarehouseOperation ();
operation . setId ( IdWorker . getId ()); operation . setSourceContainerBookingId ( bookingId );
operation . setIdempotencyKey ( key ); operation . setRequestHash ( requestHash );
operation . setResultVersion ( version + 1 ); operation . setOperatorName ( operatorName );
operation . setRemark ( request . remark () == null ? null : request . remark (). trim ());
operation . setCreatedAt ( LocalDateTime . now ()); receivingWarehouseOperations . insert ( operation );
return requireFrozenContainer ( orderMapper . findContainerProgress ( bookingId ));
}
2026-08-31 12:13:20 +08:00
private ContainerPlanProgressSummary requireFrozenContainer ( ContainerPlanProgressSummary summary ) {
2026-09-07 15:07:34 +08:00
if ( summary == null ) throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "来源柜不存在" );
2026-09-07 16:36:21 +08:00
String issue = containerSnapshotIssue ( summary );
if ( issue != null ) throw new ResponseStatusException ( HttpStatus . CONFLICT , issue );
return summary ;
}
private ContainerPlanProgressSummary containerSummaryForRead ( ContainerPlanProgressSummary summary ) {
String issue = containerSnapshotIssue ( summary );
if ( issue == null ) return summary ;
boolean invalidContainer = ! hasCanonicalContainerNo ( summary . containerNo ());
return new ContainerPlanProgressSummary ( summary . sourceContainerBookingId (), summary . containerNo (),
summary . overseasWarehouseId (), summary . overseasWarehouseCode (), summary . overseasWarehouseName (),
summary . assignmentVersion (), invalidContainer ? false : summary . canChangeReceivingWarehouse (),
invalidContainer ? issue : summary . receivingWarehouseLockReason (), summary . totalOrderCount (),
summary . inboundCodeCount (), summary . missingInboundCodeCount (), summary . planCount (),
2026-09-10 16:33:32 +08:00
summary . plannedInboundCodeCount (), summary . pendingPlanCount (), summary . resourcePendingPlanCount (), summary . plannedArrivalAt (),
2026-09-07 16:36:21 +08:00
false , issue );
}
private String containerSnapshotIssue ( ContainerPlanProgressSummary summary ) {
if ( ! hasCanonicalContainerNo ( summary . containerNo ())) {
return "来源柜真实集装箱号缺失、格式无效或存在漂移" ;
}
if ( ! StringUtils . hasText ( summary . overseasWarehouseId ())
|| ! StringUtils . hasText ( summary . overseasWarehouseCode ())
|| ! StringUtils . hasText ( summary . overseasWarehouseName ()) || summary . assignmentVersion () < 0 ) {
return "柜内接柜仓业务快照不一致" ;
}
return null ;
}
private boolean hasCanonicalContainerNo ( String containerNo ) {
2026-08-31 12:13:20 +08:00
try {
2026-09-07 16:36:21 +08:00
ContainerNumbers . requireCanonical ( containerNo );
return true ;
2026-08-31 12:13:20 +08:00
} catch ( IllegalArgumentException exception ) {
2026-09-07 16:36:21 +08:00
return false ;
2026-08-31 12:13:20 +08:00
}
2026-08-28 13:54:32 +08:00
}
2026-09-07 15:07:34 +08:00
private String normalizeBookingId ( String value ) {
String result = value == null ? null : value . trim ();
if ( ! StringUtils . hasText ( result ) || result . length () > 128 ) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "来源柜预订ID不能为空" );
}
return result ;
}
private String normalizeIdempotencyKey ( String value ) {
String result = value == null ? null : value . trim ();
if ( ! StringUtils . hasText ( result ) || result . length () > 255 ) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "Idempotency-Key 必填且不得超过255字符" );
}
return result ;
}
private int requireSingleAssignmentVersion ( List < LastlegOrder > orders ) {
Integer version = orders . get ( 0 ). getReceivingWarehouseVersion ();
if ( version == null || orders . stream (). anyMatch ( order -> ! Objects . equals ( version ,
order . getReceivingWarehouseVersion ()) || ! StringUtils . hasText ( order . getOverseasWarehouseId ())
|| ! StringUtils . hasText ( order . getOverseasWarehouseCode ())
|| ! StringUtils . hasText ( order . getOverseasWarehouseName ()))) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "柜内接柜仓分配事实不一致" );
}
return version ;
}
private String hash ( String value ) {
try {
return java . util . HexFormat . of (). formatHex ( MessageDigest . getInstance ( "SHA-256" )
. digest ( value . getBytes ( StandardCharsets . UTF_8 )));
} catch ( Exception exception ) {
throw new IllegalStateException ( exception );
}
}
2026-08-19 16:24:02 +08:00
public LastlegOrderDetailResponse detail ( String customerId , long orderId ) {
LastlegOrder order = findOwned ( customerId , orderId );
2026-10-09 11:49:17 +08:00
returnOrderReferences . populateForRead ( List . of ( order ));
refreshReturnVolumes ( List . of ( order ));
2026-08-19 16:24:02 +08:00
List < LastlegOrderDetail > cargo = detailMapper . selectList ( new LambdaQueryWrapper < LastlegOrderDetail > ()
2026-08-24 11:55:50 +08:00
. eq ( LastlegOrderDetail :: getCustomerId , order . getCustomerId ())
2026-08-19 16:24:02 +08:00
. eq ( LastlegOrderDetail :: getOrderId , orderId ). orderByAsc ( LastlegOrderDetail :: getId ));
List < LastlegOrderStatusHistory > history = historyMapper . selectList (
new LambdaQueryWrapper < LastlegOrderStatusHistory > ()
2026-08-24 11:55:50 +08:00
. eq ( LastlegOrderStatusHistory :: getCustomerId , order . getCustomerId ())
2026-08-19 16:24:02 +08:00
. eq ( LastlegOrderStatusHistory :: getOrderId , orderId )
. orderByAsc ( LastlegOrderStatusHistory :: getCreatedAt ));
2026-09-09 16:37:28 +08:00
List < OverseasFeeSource > fees = feeSources . listForBusiness ( order . getCustomerId (), "LASTLEG_ORDER" , orderId );
2026-08-26 11:05:26 +08:00
List < LastlegOrderOperationLog > operations = operationLogMapper . selectList (
new LambdaQueryWrapper < LastlegOrderOperationLog > ()
. eq ( LastlegOrderOperationLog :: getCustomerId , order . getCustomerId ())
. eq ( LastlegOrderOperationLog :: getOrderId , orderId )
. orderByAsc ( LastlegOrderOperationLog :: getCreatedAt ));
2026-09-07 23:58:18 +08:00
List < LastlegOrderCommandDtos . ManualNodeView > manualNodes = manualNodeMapper . selectList (
new LambdaQueryWrapper < LastlegOrderManualNode > ()
. eq ( LastlegOrderManualNode :: getCustomerId , order . getCustomerId ())
. eq ( LastlegOrderManualNode :: getOrderId , orderId )
. orderByAsc ( LastlegOrderManualNode :: getId )). stream ()
. map ( value -> new LastlegOrderCommandDtos . ManualNodeView (
value . getNodeCode (), value . getNodeAt (), value . getSource ()))
. sorted ( java . util . Comparator . comparingInt ( value -> MANUAL_NODE_CODES . indexOf ( value . nodeCode ()))). toList ();
2026-09-14 09:58:30 +08:00
return new LastlegOrderDetailResponse ( summary ( order , palletMergeRelations ( List . of ( order )). get ( order . getId ()),
2026-09-18 14:58:40 +08:00
containerNode ( order ), dispatchStatuses ( List . of ( order )). getOrDefault ( order . getId (), "UNSCHEDULED" ),
2026-10-09 16:12:02 +08:00
orderWeights ( List . of ( order )). get ( order . getId ()), relabelSubOrders ( List . of ( order )). getOrDefault ( order . getId (), List . of ()),
businessSources ( List . of ( order )). get ( order . getId ())), manualNodes , cargo , history . stream ()
2026-08-20 18:02:38 +08:00
. map ( value -> new LastlegOrderDetailResponse . History ( value . getId (), value . getFromStatus (),
2026-08-26 11:05:26 +08:00
value . getToStatus (), value . getRemark (), value . getCreatedAt ())). toList (), fees , operations . stream ()
. map ( value -> new LastlegOrderDetailResponse . Operation ( value . getId (), value . getOperationNode (),
2026-10-09 15:20:15 +08:00
value . getOperatorName (), value . getResult (), value . getCreatedAt ())). toList (),
"RETURN_RELABEL" . equals ( order . getOrderType ()) && StringUtils . hasText ( order . getSourceOrderNo ())
? returnReceipts . detail ( order . getCustomerId (), order . getSourceOrderNo ()) : null );
2026-08-26 11:05:26 +08:00
}
@Transactional
2026-09-09 16:37:28 +08:00
public OverseasFeeSource createFee ( String customerId , long orderId , LastlegOrderRequests . CreateFee request ,
String operatorName , String idempotencyKey ) {
2026-08-26 11:05:26 +08:00
LastlegOrder order = findOwned ( customerId , orderId );
LocalDateTime now = LocalDateTime . now ();
2026-09-09 16:37:28 +08:00
OverseasFeeSource fee = feeSources . create ( operatorName , idempotencyKey ,
new OverseasFeeSourceDtos . SaveRequest ( order . getCustomerId (), order . getCustomerName (),
"OVERSEAS_ORDER_FEE" , "LASTLEG_ORDER" , orderId , order . getOrderNo (), orderId ,
null , order . getInboundCode (), null , parseLong ( order . getSourceContainerBookingId ()),
order . getContainerNo (), parseLong ( order . getOverseasWarehouseId ()), request . feeItemCode (),
request . feeItemName (), request . direction (), request . supplierId (), request . supplierName (),
request . supplierCategory (), request . settlementMode (), request . paidAt (), request . paymentMethod (),
request . paymentReferenceNo (), request . paymentVoucherFileIds (), request . businessDate (),
request . currency (), request . amount (), request . chargeableQuantity (), request . quantityUnit (),
request . unitPrice (), request . periodStart (), request . periodEnd ()));
fee = feeSources . submit ( fee . getSourceFeeId (), operatorName , childKey ( idempotencyKey , "submit" ));
2026-08-26 11:05:26 +08:00
LastlegOrderOperationLog operation = new LastlegOrderOperationLog ();
operation . setId ( IdWorker . getId ());
operation . setCustomerId ( order . getCustomerId ());
operation . setOrderId ( orderId );
operation . setOperationNode ( "新增费用" );
operation . setOperatorName ( operatorName );
2026-09-09 16:37:28 +08:00
operation . setResult ( "新增" + fee . getFeeItemName () + ": " + fee . getOriginalCurrency () + " "
+ fee . getOriginalAmount (). toPlainString () + ",已提交财务审核" );
2026-08-26 11:05:26 +08:00
operation . setCreatedAt ( now );
operationLogMapper . insert ( operation );
return fee ;
2026-08-19 16:24:02 +08:00
}
2026-08-28 14:20:48 +08:00
@Transactional
public void setPalletMergeRequired ( String customerId , long orderId ,
LastlegOrderRequests . SetPalletMergeRequired request ,
String operatorName ) {
LastlegOrder current = findOwned ( customerId , orderId );
LastlegOrder order = orderMapper . lockById ( current . getCustomerId (), orderId );
if ( order == null ) throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "Tail-leg order not found" );
if ( "INBOUND_COMPLETED" . equals ( order . getCurrentStatus ())) {
throw new ResponseStatusException ( HttpStatus . CONFLICT ,
"Completed inbound orders cannot change the pallet merge mark" );
}
if ( PALLET_MERGED . equals ( order . getPalletMergeFlag ())) {
throw new ResponseStatusException ( HttpStatus . CONFLICT ,
"A completed pallet merge cannot be unmarked" );
}
String target = request . palletMergeRequired () ? PALLET_MERGE_REQUIRED : null ;
if ( Objects . equals ( target , order . getPalletMergeFlag ())) return ;
LocalDateTime now = LocalDateTime . now ();
if ( orderMapper . updatePalletMergeFlag ( order . getCustomerId (), orderId , target , now ) != 1 ) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "Pallet merge mark was not updated" );
}
LastlegOrderOperationLog operation = new LastlegOrderOperationLog ();
operation . setId ( IdWorker . getId ());
operation . setCustomerId ( order . getCustomerId ());
operation . setOrderId ( orderId );
operation . setOperationNode ( request . palletMergeRequired () ? "标记合托" : "取消合托标记" );
operation . setOperatorName ( operatorName );
operation . setResult ( request . palletMergeRequired ()
? "PDA扫描本单箱唛时提示需要合托" : "已取消PDA合托提示" );
operation . setCreatedAt ( now );
operationLogMapper . insert ( operation );
}
2026-08-31 14:39:40 +08:00
@Transactional
public void setPalletMergeBatch ( String customerId , LastlegOrderRequests . SetPalletMergeBatch request ,
String operatorName ) {
List < Long > ids = request . orderIds (). stream (). filter ( Objects :: nonNull ). distinct (). toList ();
if ( ids . isEmpty ()) throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "At least one order is required" );
List < LastlegOrder > orders = orderMapper . selectList ( new LambdaQueryWrapper < LastlegOrder > ()
. eq ( StringUtils . hasText ( customerId ), LastlegOrder :: getCustomerId , customerId )
. in ( LastlegOrder :: getId , ids ). last ( "FOR UPDATE" ));
if ( orders . size () != ids . size ()) {
throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "One or more tail-leg orders were not found" );
}
if ( orders . stream (). map ( LastlegOrder :: getCustomerId ). distinct (). count () != 1 ) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "Pallet merge orders must belong to one customer" );
}
if ( orders . stream (). map ( order -> StringUtils . hasText ( order . getTransferWarehouseCode ())
? order . getTransferWarehouseCode () : order . getFbaWarehouseCode ()). filter ( Objects :: nonNull ). distinct (). count () > 1 ) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "Pallet merge orders must have one destination warehouse" );
}
if ( orders . stream (). anyMatch ( order -> "INBOUND_COMPLETED" . equals ( order . getCurrentStatus ())
|| PALLET_MERGED . equals ( order . getPalletMergeFlag ()))) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "Completed pallet merge orders cannot be changed" );
}
String batchId = request . palletMergeRequired ()
? "PMG-" + java . util . UUID . randomUUID (). toString (). replace ( "-" , "" ) : null ;
String flag = request . palletMergeRequired () ? PALLET_MERGE_REQUIRED : null ;
LocalDateTime now = LocalDateTime . now ();
for ( LastlegOrder order : orders ) {
if ( orderMapper . updatePalletMergeBatch ( order . getCustomerId (), order . getId (), flag , batchId , now ) != 1 ) {
throw new ResponseStatusException ( HttpStatus . CONFLICT , "Pallet merge group was not updated" );
}
LastlegOrderOperationLog operation = new LastlegOrderOperationLog ();
operation . setId ( IdWorker . getId ()); operation . setCustomerId ( order . getCustomerId ());
operation . setOrderId ( order . getId ());
operation . setOperationNode ( request . palletMergeRequired () ? "批量标记合托" : "取消合托标记" );
operation . setOperatorName ( operatorName );
operation . setResult ( request . palletMergeRequired () ? "PDA合托分组 " + batchId : "已取消PDA合托分组" );
operation . setCreatedAt ( now ); operationLogMapper . insert ( operation );
}
2026-08-19 16:24:02 +08:00
}
@Transactional
public int transferDestination ( String customerId , LastlegOrderRequests . TransferDestination request ) {
List < Long > ids = request . orderIds (). stream (). filter ( Objects :: nonNull ). distinct (). toList ();
if ( ids . isEmpty ()) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "At least one order is required" );
}
2026-08-28 13:54:32 +08:00
FbaWarehouseValidator . VerifiedFbaWarehouse target = fbaWarehouseValidator . required (
request . warehouseId (), request . warehouseCode (), request . warehouseName ());
String warehouseId = Long . toString ( target . id ());
2026-08-19 16:24:02 +08:00
List < LastlegOrder > orders = orderMapper . selectList ( new LambdaQueryWrapper < LastlegOrder > ()
2026-08-21 10:43:25 +08:00
. eq ( StringUtils . hasText ( customerId ), LastlegOrder :: getCustomerId , customerId )
. in ( LastlegOrder :: getId , ids ). last ( "FOR UPDATE" ));
2026-08-19 16:24:02 +08:00
if ( orders . size () != ids . size ()) {
throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "One or more tail-leg orders were not found" );
}
for ( LastlegOrder order : orders ) {
ensureTransferable ( order );
}
LocalDateTime now = LocalDateTime . now ();
for ( LastlegOrder order : orders ) {
String fromStatus = order . getCurrentStatus ();
2026-08-28 13:54:32 +08:00
order . setTransferWarehouseId ( warehouseId );
order . setTransferWarehouseCode ( target . code ());
order . setTransferWarehouseName ( target . name ());
2026-08-19 16:24:02 +08:00
order . setCurrentStatus ( DESTINATION_ASSIGNED );
order . setUpdatedAt ( now );
orderMapper . updateById ( order );
LastlegOrderDetail detailUpdate = new LastlegOrderDetail ();
2026-08-28 13:54:32 +08:00
detailUpdate . setDestinationWarehouseId ( warehouseId );
detailUpdate . setDestinationWarehouseCode ( target . code ());
detailUpdate . setDestinationWarehouseName ( target . name ());
2026-08-19 16:24:02 +08:00
detailMapper . update ( detailUpdate , new LambdaQueryWrapper < LastlegOrderDetail > ()
2026-08-24 15:38:28 +08:00
. eq ( LastlegOrderDetail :: getCustomerId , order . getCustomerId ())
2026-08-19 16:24:02 +08:00
. eq ( LastlegOrderDetail :: getOrderId , order . getId ()));
2026-08-24 15:38:28 +08:00
InboundPlanCarton cartonUpdate = new InboundPlanCarton ();
2026-08-28 13:54:32 +08:00
cartonUpdate . setDestinationWarehouseId ( warehouseId );
cartonUpdate . setDestinationWarehouseCode ( target . code ());
cartonUpdate . setDestinationWarehouseName ( target . name ());
2026-08-24 15:38:28 +08:00
cartonMapper . update ( cartonUpdate , new LambdaQueryWrapper < InboundPlanCarton > ()
. eq ( InboundPlanCarton :: getCustomerId , order . getCustomerId ())
. eq ( InboundPlanCarton :: getOrderId , order . getId ()));
2026-08-19 16:24:02 +08:00
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory ();
2026-08-24 11:55:50 +08:00
history . setCustomerId ( order . getCustomerId ());
2026-08-19 16:24:02 +08:00
history . setOrderId ( order . getId ());
history . setFromStatus ( fromStatus );
history . setToStatus ( DESTINATION_ASSIGNED );
history . setRemark ( StringUtils . hasText ( request . remark ())
2026-08-28 13:54:32 +08:00
? request . remark () : "Destination warehouse assigned: " + target . code ());
2026-08-19 16:24:02 +08:00
history . setCreatedAt ( now );
historyMapper . insert ( history );
}
return orders . size ();
}
private LambdaQueryWrapper < LastlegOrder > filter ( String customerId , LastlegOrderRequests . Query query ) {
2026-09-14 09:58:30 +08:00
return filter ( customerId , query , null );
}
private LambdaQueryWrapper < LastlegOrder > filter ( String customerId , LastlegOrderRequests . Query query , String relabelNo ) {
2026-08-19 16:24:02 +08:00
if ( query . createdFrom () != null && query . createdTo () != null && query . createdFrom (). isAfter ( query . createdTo ())) {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "createdFrom must not be after createdTo" );
}
2026-08-21 14:42:40 +08:00
String currentStatus = StringUtils . hasText ( query . currentStatus ()) ? query . currentStatus () : query . status ();
2026-09-08 13:14:13 +08:00
String warehouse = StringUtils . hasText ( query . warehouse ()) ? query . warehouse (). trim () : null ;
2026-08-19 16:24:02 +08:00
return new LambdaQueryWrapper < LastlegOrder > ()
2026-10-08 15:55:47 +08:00
. ne ( LastlegOrder :: getOrderType , "CARGO_DISCARD" )
2026-08-21 10:43:25 +08:00
. eq ( StringUtils . hasText ( customerId ), LastlegOrder :: getCustomerId , customerId )
2026-09-14 15:03:04 +08:00
. and ( StringUtils . hasText ( query . orderNo ()), nested -> nested
. like ( LastlegOrder :: getOrderNo , query . orderNo ())
. or ()
. like ( LastlegOrder :: getSourceOrderNo , query . orderNo ()))
2026-09-22 08:49:21 +08:00
. and ( StringUtils . hasText ( relabelNo ), nested -> nested
. like ( LastlegOrder :: getRelabelOrderNo , relabelNo )
. or (). apply ( "EXISTS (SELECT 1 FROM ll_relabel_application r WHERE r.lastleg_order_id=ll_order.id " +
"AND r.customer_id=ll_order.customer_id AND r.relabel_no LIKE CONCAT('%',{0},'%'))" , relabelNo ))
2026-08-21 14:42:40 +08:00
. eq ( StringUtils . hasText ( query . orderType ()), LastlegOrder :: getOrderType , query . orderType ())
2026-10-09 16:31:19 +08:00
. apply ( StringUtils . hasText ( query . businessType ()), LastlegOrderMapper . BUSINESS_TYPE_FILTER_SQL , query . businessType ())
. apply ( StringUtils . hasText ( query . businessOrderNo ()), LastlegOrderMapper . BUSINESS_ORDER_NO_FILTER_SQL , query . businessOrderNo ())
2026-10-09 16:12:02 +08:00
. apply ( StringUtils . hasText ( query . externalBookingNo ()), LastlegOrderMapper . EXTERNAL_BOOKING_NO_SQL + " LIKE CONCAT('%',{0},'%')" , query . externalBookingNo ())
2026-10-08 15:12:47 +08:00
. apply ( query . sailingStage () != null , LastlegOrderMapper . SAILING_STAGE_FILTER_SQL ,
query . sailingStage () == LastlegOrderRequests . SailingStage . AFTER_DEPARTURE ? 1 : 0 )
2026-08-19 16:24:02 +08:00
. like ( StringUtils . hasText ( query . inboundCode ()), LastlegOrder :: getInboundCode , query . inboundCode ())
2026-09-06 16:02:11 +08:00
. like ( StringUtils . hasText ( query . lxCode ()), LastlegOrder :: getLxCode , query . lxCode ())
2026-08-19 16:24:02 +08:00
. like ( StringUtils . hasText ( query . sourceOrderNo ()), LastlegOrder :: getSourceOrderNo , query . sourceOrderNo ())
. like ( StringUtils . hasText ( query . containerNo ()), LastlegOrder :: getContainerNo , query . containerNo ())
2026-08-21 14:42:40 +08:00
. eq ( StringUtils . hasText ( currentStatus ), LastlegOrder :: getCurrentStatus , currentStatus )
2026-09-08 13:14:13 +08:00
. apply ( StringUtils . hasText ( warehouse ), LastlegOrderMapper . WAREHOUSE_FILTER_SQL , warehouse )
2026-08-19 16:24:02 +08:00
. eq ( StringUtils . hasText ( query . transferWarehouseCode ()), LastlegOrder :: getTransferWarehouseCode ,
query . transferWarehouseCode ())
. eq ( query . abnormal () != null , LastlegOrder :: getAbnormal , query . abnormal ())
. eq ( query . detained () != null , LastlegOrder :: getDetained , query . detained ())
. ge ( query . createdFrom () != null , LastlegOrder :: getCreatedAt , query . createdFrom ())
. le ( query . createdTo () != null , LastlegOrder :: getCreatedAt , query . createdTo ());
}
private LastlegOrder findOwned ( String customerId , long orderId ) {
LastlegOrder order = orderMapper . selectOne ( new LambdaQueryWrapper < LastlegOrder > ()
2026-08-21 10:43:25 +08:00
. eq ( StringUtils . hasText ( customerId ), LastlegOrder :: getCustomerId , customerId )
. eq ( LastlegOrder :: getId , orderId ));
2026-08-19 16:24:02 +08:00
if ( order == null ) throw new ResponseStatusException ( HttpStatus . NOT_FOUND , "Tail-leg order not found" );
return order ;
}
private void ensureTransferable ( LastlegOrder order ) {
if ( StringUtils . hasText ( order . getInboundOrderNo ()) || INBOUND_PLANNED . equals ( order . getCurrentStatus ())) {
throw new ResponseStatusException ( HttpStatus . CONFLICT ,
"Destination warehouse cannot be changed after inbound plan creation: " + order . getId ());
}
}
2026-08-28 16:30:02 +08:00
private Map < Long , List < LastlegOrderMapper . PalletMergeRelationRow >> palletMergeRelations ( List < LastlegOrder > orders ) {
if ( orders . isEmpty ()) return Map . of ();
Map < Long , List < LastlegOrderMapper . PalletMergeRelationRow >> result = new LinkedHashMap <> ();
List < LastlegOrderMapper . PalletMergeRelationRow > rows = orderMapper . findPalletMergeRelations (
orders . stream (). map ( LastlegOrder :: getId ). toList ());
if ( rows == null ) return Map . of ();
for ( LastlegOrderMapper . PalletMergeRelationRow row : rows ) {
result . computeIfAbsent ( row . orderId (), ignored -> new ArrayList <> ()). add ( row );
}
return result ;
}
2026-09-14 09:58:30 +08:00
private Map < Long , String > dispatchStatuses ( List < LastlegOrder > orders ) {
if ( orders . isEmpty ()) return Map . of ();
List < LastlegOrderMapper . DispatchStatusRow > rows = orderMapper . findDispatchStatuses (
orders . stream (). map ( LastlegOrder :: getId ). toList ());
if ( rows == null ) return Map . of ();
return rows . stream (). collect ( Collectors . toMap ( LastlegOrderMapper . DispatchStatusRow :: orderId ,
LastlegOrderMapper . DispatchStatusRow :: dispatchStatus ));
}
2026-10-09 11:49:17 +08:00
private void refreshReturnVolumes ( List < LastlegOrder > orders ) {
List < Long > ids = orders . stream (). filter ( order -> "RETURN_RELABEL" . equals ( order . getOrderType ())). map ( LastlegOrder :: getId ). toList ();
if ( ids . isEmpty ()) return ;
Map < Long , BigDecimal > volumes = orderMapper . findReturnVolumes ( ids ). stream ()
. collect ( Collectors . toMap ( LastlegOrderMapper . ReturnVolumeRow :: orderId , LastlegOrderMapper . ReturnVolumeRow :: volumeCbm ));
for ( LastlegOrder order : orders ) if ( volumes . containsKey ( order . getId ())) order . setVolumeCbm ( volumes . get ( order . getId ()));
}
2026-09-18 14:58:40 +08:00
private Map < Long , LastlegOrderMapper . OrderWeightRow > orderWeights ( List < LastlegOrder > orders ) {
if ( orders . isEmpty ()) return Map . of ();
List < LastlegOrderMapper . OrderWeightRow > rows = orderMapper . findOrderWeights (
orders . stream (). map ( LastlegOrder :: getId ). toList ());
if ( rows == null ) return Map . of ();
return rows . stream (). collect ( Collectors . toMap ( LastlegOrderMapper . OrderWeightRow :: orderId , row -> row ));
}
2026-09-22 08:49:21 +08:00
private Map < Long , List < com . baiyue . lastleg . dto . RelabelSubOrder >> relabelSubOrders ( List < LastlegOrder > orders ) {
if ( orders . isEmpty ()) return Map . of ();
var rows = orderMapper . findRelabelSubOrders ( orders . stream (). map ( LastlegOrder :: getId ). toList ());
return rows == null ? Map . of () : rows . stream (). collect ( Collectors . groupingBy ( com . baiyue . lastleg . dto . RelabelSubOrder :: lastlegOrderId ));
}
2026-10-09 16:12:02 +08:00
private Map < Long , LastlegOrderSummary . BusinessSource > businessSources ( List < LastlegOrder > orders ) {
if ( orders . isEmpty ()) return Map . of ();
return orderMapper . findBusinessSources ( orders . stream (). map ( LastlegOrder :: getId ). toList ()). stream ()
. collect ( Collectors . toMap ( LastlegOrderMapper . BusinessSourceRow :: orderId , row -> {
String label = "US_TRUCK" . equals ( row . businessType ()) ? "本土卡车" : LastlegOrderType . labelOf ( row . businessType ());
if ( row . poolSource ()) label += " · 拼车需求池" ;
else if ( "LOCAL_LOOSEBOARD" . equals ( row . businessType ())) label += " · 独立下单" ;
else if ( StringUtils . hasText ( row . externalBookingNo ())) label += " · 外接来货" ;
return new LastlegOrderSummary . BusinessSource ( row . businessType (), label , row . businessOrderNo (), row . externalBookingNo ());
}));
}
2026-09-14 09:58:30 +08:00
private LastlegOrderSummary summary ( LastlegOrder o , List < LastlegOrderMapper . PalletMergeRelationRow > relations ,
2026-09-18 14:58:40 +08:00
String containerNode , String dispatchStatus ,
2026-09-22 08:49:21 +08:00
LastlegOrderMapper . OrderWeightRow weight ,
2026-10-09 16:12:02 +08:00
List < com . baiyue . lastleg . dto . RelabelSubOrder > relabelApplications ,
LastlegOrderSummary . BusinessSource businessSource ) {
2026-08-28 16:30:02 +08:00
List < LastlegOrderMapper . PalletMergeRelationRow > related = relations == null ? List . of () : relations ;
2026-09-14 15:03:04 +08:00
return new LastlegOrderSummary ( o . getId (), displayOrderNo ( o ), o . getOrderType (),
2026-08-24 17:15:48 +08:00
LastlegOrderType . labelOf ( o . getOrderType ()), o . getDestinationType (), o . getSourceInboundId (),
2026-09-15 16:39:44 +08:00
displayInboundCode ( o ), o . getLxCode (), o . getSourceOrderNo (), o . getCustomerId (), o . getCustomerName (),
o . getSourceContainerBookingId (), o . getOverseasWarehouseId (),
2026-09-07 15:07:34 +08:00
o . getOverseasWarehouseCode (), o . getOverseasWarehouseName (), o . getTransferWarehouseId (), o . getTransferWarehouseCode (),
2026-09-11 16:17:16 +08:00
o . getTransferWarehouseName (), o . getFbaWarehouseCode (), o . getContainerNo (), containerNode ,
2026-09-07 23:58:18 +08:00
o . getAbnormal (), o . getDetained (), o . getHoldChargeable (), o . getHoldReason (),
Boolean . TRUE . equals ( o . getManualNodeOverride ()), Boolean . TRUE . equals ( o . getManualNodeOverride ()) ? "MANUAL" : "CONTAINER" ,
2026-09-17 17:37:29 +08:00
o . getVersion () == null ? 0 : o . getVersion (), o . getPalletCount (), o . getPackageCount (),
2026-09-18 14:58:40 +08:00
o . getReceivedPackageCount (), packageDifference ( o ), weight == null ? BigDecimal . ZERO : weight . grossWeightKg (),
weight == null ? BigDecimal . ZERO : weight . netWeightKg (), o . getVolumeCbm (),
2026-08-19 16:24:02 +08:00
o . getProductQuantity (), o . getVolumeWeightKg (), o . getEstimatedArrivalAt (), o . getActualArrivalAt (),
2026-09-14 20:21:07 +08:00
o . getEstimatedWarehouseAt (), o . getInboundOrderNo (), dispatchStatus , o . getRelabelOrderNo (), relabelBusinessStatus ( o . getRelabelStatus ()),
2026-08-28 16:30:02 +08:00
o . getPalletMergeFlag (), related . stream (). map ( LastlegOrderMapper . PalletMergeRelationRow :: mergeGroupNo ). distinct (). findFirst (). orElse ( null ),
2026-08-31 14:39:40 +08:00
related . stream (). map ( LastlegOrderMapper . PalletMergeRelationRow :: relatedInboundNo ). filter ( Objects :: nonNull ). distinct (). toList (),
2026-08-28 16:30:02 +08:00
o . getCurrentStatus (), o . getSubOrderStatus (), o . getPrivateRemark (),
2026-10-09 16:12:02 +08:00
o . getCustomerRemark (), o . getWarehouseRemark (), o . getCreatedAt (), o . getUpdatedAt (), relabelApplications , businessSource );
2026-08-19 16:24:02 +08:00
}
2026-09-17 17:37:29 +08:00
private static Integer packageDifference ( LastlegOrder order ) {
if ( order . getReceivedPackageCount () == null ) return null ;
return order . getReceivedPackageCount () - java . util . Objects . requireNonNullElse ( order . getPackageCount (), 0 );
}
2026-09-14 15:03:04 +08:00
static String displayOrderNo ( LastlegOrder order ) {
2026-10-09 15:20:15 +08:00
return ( "WAREHOUSE_TRANSFER" . equals ( order . getOrderType ()) || "RETURN_RELABEL" . equals ( order . getOrderType ())) && StringUtils . hasText ( order . getSourceOrderNo ())
2026-09-14 15:03:04 +08:00
? order . getSourceOrderNo () : order . getOrderNo ();
}
2026-09-11 16:17:16 +08:00
private String containerNode ( LastlegOrder order ) {
if ( ! StringUtils . hasText ( order . getSourceContainerBookingId ())) return null ;
ContainerShippingGate gate = containerShippingGates . selectById ( order . getSourceContainerBookingId ());
return gate == null ? null : gate . getStandardNode ();
}
private Map < String , String > containerNodes ( List < LastlegOrder > orders ) {
List < String > bookingIds = orders . stream (). map ( LastlegOrder :: getSourceContainerBookingId )
. filter ( StringUtils :: hasText ). distinct (). toList ();
if ( bookingIds . isEmpty ()) return Map . of ();
Map < String , String > result = new LinkedHashMap <> ();
containerShippingGates . selectBatchIds ( bookingIds ). forEach ( gate -> {
if ( StringUtils . hasText ( gate . getStandardNode ())) {
result . put ( gate . getContainerBookingId (), gate . getStandardNode ());
}
});
return result ;
}
2026-09-07 12:30:17 +08:00
private static String displayInboundCode ( LastlegOrder order ) {
String orderType = order . getOrderType ();
return "RELABEL_APPLICATION" . equals ( orderType ) || "LOCAL_LOOSEBOARD" . equals ( orderType )
? null : order . getInboundCode ();
}
2026-09-14 20:21:07 +08:00
private static String relabelBusinessStatus ( String workflowStatus ) {
2026-09-15 15:15:46 +08:00
if ( "PROCESSING" . equals ( workflowStatus )) return "PROCESSING" ;
2026-09-14 20:21:07 +08:00
if ( ! StringUtils . hasText ( workflowStatus ) || "NOT_REQUIRED" . equals ( workflowStatus )) return workflowStatus ;
return Set . of ( "READY_FOR_OUTBOUND" , "COMPLETED" ). contains ( workflowStatus )
? "COMPLETED" : "PENDING_PROCESSING" ;
}
2026-09-09 16:37:28 +08:00
private static Long parseLong ( String value ) {
try { return StringUtils . hasText ( value ) ? Long . parseLong ( value . trim ()) : null ; }
catch ( NumberFormatException ignored ) { return null ; }
}
private static String childKey ( String key , String suffix ) {
String prefix = key == null ? "" : key . trim ();
int maxPrefix = Math . max ( 0 , 128 - suffix . length () - 1 );
return prefix . substring ( 0 , Math . min ( prefix . length (), maxPrefix )) + ':' + suffix ;
}
2026-08-19 16:24:02 +08:00
}