perf(firstleg): batch appointment source and execution facts
This commit is contained in:
+13
-5
@@ -10,6 +10,10 @@ public interface AppointmentWorkSourceMapper {
|
||||
String destinationWarehouseName, LocalDateTime departedLocalAt, LocalDateTime sailedEvidenceLocalAt, int version, long factVersion, long deliveryVersion) { }
|
||||
record Cargo(long inboundId, long customerId, long orderId, String orderNo, String inboundCode,
|
||||
String destinationKind, String destinationCode, String destinationName, int packageCount) { }
|
||||
record ContainerCargo(long containerBookingId,long inboundId,long customerId,long orderId,String orderNo,String inboundCode,
|
||||
String destinationKind,String destinationCode,String destinationName,int packageCount) {
|
||||
public Cargo cargo() { return new Cargo(inboundId,customerId,orderId,orderNo,inboundCode,destinationKind,destinationCode,destinationName,packageCount); }
|
||||
}
|
||||
|
||||
String SOURCES = """
|
||||
SELECT b.id,b.cabinet_no,b.container_no,b.departure_port_name,b.destination_warehouse_name,
|
||||
@@ -29,15 +33,19 @@ public interface AppointmentWorkSourceMapper {
|
||||
List<Source> sources();
|
||||
|
||||
@Select("""
|
||||
SELECT i.inbound_id, i.account_id customer_id, i.order_id, i.order_no,i.inbound_code,
|
||||
<script>
|
||||
SELECT p.container_booking_id,i.inbound_id, i.account_id customer_id, i.order_id, i.order_no,i.inbound_code,
|
||||
o.destination_type destination_kind,i.destination_warehouse_code destination_code,
|
||||
i.destination_warehouse_name destination_name,SUM(i.package_count) package_count
|
||||
FROM fl_loading_plan p JOIN fl_loading_plan_item i ON i.loading_plan_id=p.id AND i.deleted=0
|
||||
JOIN fl_inbound_order o ON o.id=i.inbound_id
|
||||
WHERE p.container_booking_id=#{id} AND p.plan_status='COMPLETED'
|
||||
GROUP BY i.inbound_id,i.account_id,i.order_id,i.order_no,i.inbound_code,o.destination_type,
|
||||
WHERE p.container_booking_id IN
|
||||
<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
AND p.plan_status='COMPLETED'
|
||||
GROUP BY p.container_booking_id,i.inbound_id,i.account_id,i.order_id,i.order_no,i.inbound_code,o.destination_type,
|
||||
i.destination_warehouse_code,i.destination_warehouse_name
|
||||
ORDER BY i.inbound_id
|
||||
ORDER BY p.container_booking_id,i.inbound_id
|
||||
</script>
|
||||
""")
|
||||
List<Cargo> cargo(@Param("id") long id);
|
||||
List<ContainerCargo> cargo(@Param("ids") List<Long> ids);
|
||||
}
|
||||
|
||||
+25
-17
@@ -13,8 +13,8 @@ import java.util.*;
|
||||
@Service
|
||||
public class AppointmentWorkSourceService {
|
||||
public interface OriginalFacts {
|
||||
String establishedFlow(long containerBookingId);
|
||||
Fact read(FactRequest request, ContainerSource source);
|
||||
Map<Long,String> establishedFlows(List<Long> containerBookingIds);
|
||||
List<Fact> read(List<FactRequest> requests, Map<Long,ContainerSource> sources);
|
||||
}
|
||||
private final AppointmentWorkSourceMapper mapper;
|
||||
private final OriginalFacts original;
|
||||
@@ -37,17 +37,23 @@ public class AppointmentWorkSourceService {
|
||||
var page=rows.stream().filter(row->row.id()>cursor).limit(size+1L).toList();
|
||||
boolean complete=page.size()<=size;
|
||||
var selected=page.stream().limit(size).toList();
|
||||
return new SourcePage(selected.stream().map(this::source).toList(),complete?null:scope+":"+selected.get(selected.size()-1).id(),complete,scope);
|
||||
return new SourcePage(sources(selected),complete?null:scope+":"+selected.get(selected.size()-1).id(),complete,scope);
|
||||
}
|
||||
public ContainerSource container(long id) {
|
||||
return container(id,mapper.sources());
|
||||
}
|
||||
private ContainerSource container(long id,List<AppointmentWorkSourceMapper.Source> rows) {
|
||||
return rows.stream().filter(row->row.id()==id).findFirst().map(this::source)
|
||||
var row=mapper.sources().stream().filter(candidate->candidate.id()==id).findFirst()
|
||||
.orElseThrow(()->new ResponseStatusException(HttpStatus.NOT_FOUND,"未找到有效已开船柜次"));
|
||||
return sources(List.of(row)).get(0);
|
||||
}
|
||||
private ContainerSource source(AppointmentWorkSourceMapper.Source row) {
|
||||
var cargo=mapper.cargo(row.id());
|
||||
private List<ContainerSource> sources(List<AppointmentWorkSourceMapper.Source> rows) {
|
||||
if(rows.isEmpty()) return List.of();
|
||||
var ids=rows.stream().map(AppointmentWorkSourceMapper.Source::id).toList();
|
||||
var cargo=new HashMap<Long,List<AppointmentWorkSourceMapper.Cargo>>();
|
||||
for(var item:mapper.cargo(ids)) cargo.computeIfAbsent(item.containerBookingId(),ignored->new ArrayList<>()).add(item.cargo());
|
||||
var flows=original.establishedFlows(ids);
|
||||
return rows.stream().map(row->source(row,cargo.getOrDefault(row.id(),List.of()),flows.get(row.id()))).toList();
|
||||
}
|
||||
private ContainerSource source(AppointmentWorkSourceMapper.Source row,List<AppointmentWorkSourceMapper.Cargo> cargo,String flow) {
|
||||
if(flow==null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"柜次路线事实读取不完整");
|
||||
var requirements=cargo.stream().map(c->new Requirement("INBOUND:"+c.inboundId(),null,null,
|
||||
c.destinationKind(),row.departurePortName(),c.destinationName(),null,
|
||||
List.of(new Reference("CUSTOMER",Long.toString(c.customerId()),null,"所属客户"),
|
||||
@@ -55,20 +61,22 @@ public class AppointmentWorkSourceService {
|
||||
new Reference("INBOUND",Long.toString(c.inboundId()),c.inboundCode(),"入仓单"),
|
||||
new Reference("CONTAINER",Long.toString(row.id()),row.cabinetNo(),"柜次")),List.of(),List.of())).toList();
|
||||
return new ContainerSource(Long.toString(row.id()),row.containerNo(),null,row.departedLocalAt(),row.sailedEvidenceLocalAt(),
|
||||
hash(List.of(row,cargo)),original.establishedFlow(row.id()),null,requirements);
|
||||
hash(List.of(row,cargo)),flow,null,requirements);
|
||||
}
|
||||
public List<Fact> facts(List<FactRequest> requests) {
|
||||
if(requests==null || requests.isEmpty() || requests.size()>200) throw bad("事实请求须包含1至200项");
|
||||
Set<String> keys=new HashSet<>(); Map<Long,ContainerSource> sources=new HashMap<>();
|
||||
// A fact batch shares one authoritative manifest, instead of scanning every sailed container per item.
|
||||
var rows=mapper.sources();
|
||||
return requests.stream().map(request->{
|
||||
Set<String> keys=new HashSet<>(); var ids=new LinkedHashSet<Long>();
|
||||
for(var request:requests) {
|
||||
if(request==null || request.workItemKey()==null || request.workItemKey().isBlank() || !keys.add(request.workItemKey())
|
||||
|| !("PICKUP".equals(request.type()) || "EMPTY_RETURN".equals(request.type()) || "DELIVERY".equals(request.type()) && "DIRECT".equals(request.flow())))
|
||||
throw bad("事项身份或事实类型无效");
|
||||
long id=id(request.containerBookingId());
|
||||
return original.read(request,sources.computeIfAbsent(id,key->container(key,rows)));
|
||||
}).toList();
|
||||
ids.add(id(request.containerBookingId()));
|
||||
}
|
||||
var selected=mapper.sources().stream().filter(row->ids.contains(row.id())).toList();
|
||||
if(selected.size()!=ids.size()) throw new ResponseStatusException(HttpStatus.NOT_FOUND,"未找到有效已开船柜次");
|
||||
var sources=new LinkedHashMap<Long,ContainerSource>();
|
||||
sources(selected).forEach(source->sources.put(Long.parseLong(source.containerBookingId()),source));
|
||||
return original.read(requests,sources);
|
||||
}
|
||||
public String hash(Object value) {
|
||||
try {return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json.writeValueAsBytes(value)));}
|
||||
|
||||
+35
@@ -10,6 +10,41 @@ public interface DirectDeliveryMapper {
|
||||
int version,LocalDateTime actualAt,String timezone,String receivedBy,String evidenceNo,String remark,
|
||||
String idempotencyKey,String requestHash) { }
|
||||
record Container(long id,String recordStatus,LocalDateTime warehouseUnloadedAt) { }
|
||||
record FlowEvidence(long id,LocalDateTime warehouseUnloadedAt,long evidenceCount) { }
|
||||
record ContainerDelivery(long containerBookingId,long inboundId,String inboundCode,String orderNo,String destinationName,int packageCount,
|
||||
int version,LocalDateTime actualAt,String timezone,String receivedBy,String evidenceNo,String remark,
|
||||
String idempotencyKey,String requestHash) {
|
||||
public Delivery delivery(){return new Delivery(inboundId,inboundCode,orderNo,destinationName,packageCount,version,
|
||||
actualAt,timezone,receivedBy,evidenceNo,remark,idempotencyKey,requestHash);}
|
||||
}
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT c.id,c.unloading_completed_at warehouse_unloaded_at,COUNT(d.inbound_id) evidence_count
|
||||
FROM fl_container_booking c LEFT JOIN fl_direct_delivery_unloading d ON d.container_booking_id=c.id
|
||||
WHERE c.id IN <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY c.id,c.unloading_completed_at
|
||||
</script>
|
||||
""")
|
||||
List<FlowEvidence> flowEvidence(@Param("ids") List<Long> ids);
|
||||
@Select("""
|
||||
<script>SELECT id,record_status,unloading_completed_at warehouse_unloaded_at FROM fl_container_booking
|
||||
WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach></script>
|
||||
""")
|
||||
List<Container> containers(@Param("ids") List<Long> ids);
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT p.container_booking_id,i.inbound_id,MAX(i.inbound_code) inbound_code,MAX(i.order_no) order_no,
|
||||
MAX(i.destination_warehouse_name) destination_name,SUM(i.package_count) package_count,
|
||||
COALESCE(d.version,0) version,d.actual_at,d.timezone,d.received_by,d.evidence_no,d.remark,d.idempotency_key,d.request_hash
|
||||
FROM fl_loading_plan p JOIN fl_loading_plan_item i ON i.loading_plan_id=p.id AND i.deleted=0
|
||||
LEFT JOIN fl_direct_delivery_unloading d ON d.container_booking_id=p.container_booking_id AND d.inbound_id=i.inbound_id
|
||||
WHERE p.container_booking_id IN <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
AND p.plan_status='COMPLETED'
|
||||
GROUP BY p.container_booking_id,i.inbound_id,d.version,d.actual_at,d.timezone,d.received_by,d.evidence_no,d.remark,d.idempotency_key,d.request_hash
|
||||
ORDER BY p.container_booking_id,i.inbound_id
|
||||
</script>
|
||||
""")
|
||||
List<ContainerDelivery> deliveriesBatch(@Param("ids") List<Long> ids);
|
||||
@Select("SELECT id,record_status,unloading_completed_at warehouse_unloaded_at FROM fl_container_booking WHERE id=#{id} FOR UPDATE")
|
||||
@Options(flushCache=Options.FlushCachePolicy.TRUE,useCache=false)
|
||||
Container lockContainer(@Param("id") long id);
|
||||
|
||||
+10
@@ -114,6 +114,16 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
ORDER BY created_at DESC,id DESC
|
||||
""")
|
||||
List<History> histories(@Param("containerBookingId") long containerBookingId);
|
||||
record PickupChanges(long containerBookingId,long schedules,long fleets) { }
|
||||
@Select("""
|
||||
<script>SELECT container_booking_id,
|
||||
SUM(CASE WHEN action='SCHEDULE_PICKUP' THEN 1 ELSE 0 END) schedules,
|
||||
SUM(CASE WHEN action='SELECT_FLEET' THEN 1 ELSE 0 END) fleets
|
||||
FROM fl_terminal_drayage_history WHERE container_booking_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY container_booking_id</script>
|
||||
""")
|
||||
List<PickupChanges> pickupChanges(@Param("ids") List<Long> ids);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_terminal_drayage_history(id,drayage_id,container_booking_id,action,from_status,to_status,
|
||||
|
||||
+42
-15
@@ -3,6 +3,8 @@ package com.baiyue.firstleg.terminaldrayage.service;
|
||||
import com.baiyue.firstleg.portschedule.dto.AppointmentWorkSourceDtos.*;
|
||||
import com.baiyue.firstleg.portschedule.service.AppointmentWorkSourceService.OriginalFacts;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.terminaldrayage.entity.TerminalDrayageEntity;
|
||||
import com.baiyue.firstleg.terminaldrayage.mapper.DirectDeliveryMapper;
|
||||
import com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -26,17 +28,43 @@ public class AppointmentWorkTerminalFacts implements OriginalFacts {
|
||||
CarrierScheduleQueryService nodes,UnloadingEvidenceGuard unloading,ObjectMapper json) {
|
||||
this.terminal=terminal;this.direct=direct;this.nodes=nodes;this.unloading=unloading;this.json=json;
|
||||
}
|
||||
public String establishedFlow(long id) {
|
||||
var container=direct.container(id);
|
||||
boolean transfer=container!=null && container.warehouseUnloadedAt()!=null;
|
||||
boolean delivery=direct.evidenceCount(id)>0;
|
||||
if(transfer && delivery) throw new ResponseStatusException(HttpStatus.CONFLICT,"货柜存在冲突的中转和直送执行事实");
|
||||
return transfer?"TRANSFER":delivery?"DIRECT":"UNKNOWN";
|
||||
public Map<Long,String> establishedFlows(List<Long> ids) {
|
||||
var result=new HashMap<Long,String>();
|
||||
if(ids.isEmpty()) return result;
|
||||
for(var row:direct.flowEvidence(ids)) {
|
||||
boolean transfer=row.warehouseUnloadedAt()!=null;
|
||||
boolean delivery=row.evidenceCount()>0;
|
||||
if(transfer && delivery) throw new ResponseStatusException(HttpStatus.CONFLICT,"货柜存在冲突的中转和直送执行事实");
|
||||
result.put(row.id(),transfer?"TRANSFER":delivery?"DIRECT":"UNKNOWN");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public Fact read(FactRequest request,ContainerSource source) {
|
||||
long id=Long.parseLong(request.containerBookingId());
|
||||
var node=nodes.containerActualFacts(List.of(id)).stream().findFirst().orElse(null);
|
||||
var plan=terminal.find(id);
|
||||
public List<Fact> read(List<FactRequest> requests,Map<Long,ContainerSource> sources) {
|
||||
var ids=requests.stream().map(r->Long.parseLong(r.containerBookingId())).distinct().toList();
|
||||
if(ids.isEmpty()) return List.of();
|
||||
var actuals=new HashMap<Long,ContainerActualFacts>();
|
||||
for(var row:nodes.containerActualFacts(ids)) actuals.put(row.containerBookingId(),row);
|
||||
var plans=new HashMap<Long,TerminalDrayageEntity>();
|
||||
for(var row:terminal.findAll(ids)) plans.put(row.getContainerBookingId(),row);
|
||||
var pickupIds=requests.stream().filter(r->"PICKUP".equals(r.type())).map(r->Long.parseLong(r.containerBookingId())).distinct().toList();
|
||||
var changes=new HashMap<Long,TerminalDrayageMapper.PickupChanges>();
|
||||
if(!pickupIds.isEmpty()) for(var row:terminal.pickupChanges(pickupIds)) changes.put(row.containerBookingId(),row);
|
||||
var deliveryIds=requests.stream().filter(r->"DELIVERY".equals(r.type())).map(r->Long.parseLong(r.containerBookingId())).distinct().toList();
|
||||
var deliveries=new HashMap<Long,List<DirectDeliveryMapper.Delivery>>();
|
||||
if(!deliveryIds.isEmpty()) for(var row:direct.deliveriesBatch(deliveryIds))
|
||||
deliveries.computeIfAbsent(row.containerBookingId(),ignored->new ArrayList<>()).add(row.delivery());
|
||||
var returnIds=requests.stream().filter(r->"EMPTY_RETURN".equals(r.type())).map(r->Long.parseLong(r.containerBookingId())).distinct()
|
||||
.filter(id->(actuals.get(id)==null || actuals.get(id).emptyReturnedAt()==null)
|
||||
&& (plans.get(id)==null || plans.get(id).getActualEmptyReturnAt()==null)).toList();
|
||||
var completed=returnIds.isEmpty()?Map.<Long,LocalDateTime>of():unloading.completedUnloading(returnIds);
|
||||
return requests.stream().map(request->{
|
||||
long id=Long.parseLong(request.containerBookingId());
|
||||
return read(request,sources.get(id),actuals.get(id),plans.get(id),changes.get(id),
|
||||
deliveries.getOrDefault(id,List.of()),completed.get(id));
|
||||
}).toList();
|
||||
}
|
||||
private Fact read(FactRequest request,ContainerSource source,ContainerActualFacts node,TerminalDrayageEntity plan,
|
||||
TerminalDrayageMapper.PickupChanges changes,List<DirectDeliveryMapper.Delivery> deliveries,LocalDateTime completedUnloading) {
|
||||
var references=new ArrayList<Reference>();
|
||||
source.requirements().forEach(r->r.references().forEach(ref->{if(!references.contains(ref))references.add(ref);}));
|
||||
var blocks=new ArrayList<String>(); var missing=new ArrayList<String>();
|
||||
@@ -53,9 +81,8 @@ public class AppointmentWorkTerminalFacts implements OriginalFacts {
|
||||
if(plan!=null && plan.getFleetSupplierId()!=null)
|
||||
responsibility=new Responsibility("SUPPLIER",plan.getFleetSupplierId().toString(),plan.getFleetSupplierName());
|
||||
// The initial arrangement/assignment establishes a baseline. Only later changes invalidate dependants.
|
||||
var history=terminal.histories(id);
|
||||
long reschedules=Math.max(0,history.stream().filter(h->"SCHEDULE_PICKUP".equals(h.action())).count()-1);
|
||||
long reassignments=Math.max(0,history.stream().filter(h->"SELECT_FLEET".equals(h.action())).count()-1);
|
||||
long reschedules=Math.max(0,(changes==null?0:changes.schedules())-1);
|
||||
long reassignments=Math.max(0,(changes==null?0:changes.fleets())-1);
|
||||
impact=reschedules+":"+reassignments;
|
||||
if(at==null) {
|
||||
if(node==null || node.arrivedDestinationPortAt()==null) blocks.add("实际到港后才能安排提柜");
|
||||
@@ -67,7 +94,7 @@ public class AppointmentWorkTerminalFacts implements OriginalFacts {
|
||||
case "EMPTY_RETURN" -> {
|
||||
at=node==null?null:node.emptyReturnedAt();
|
||||
if(at==null && plan!=null) at=plan.getActualEmptyReturnAt();
|
||||
if(at==null && unloading.completedUnloading(id)==null) blocks.add("海外仓卸柜或全部直送目的仓实际卸货后才能还空");
|
||||
if(at==null && completedUnloading==null) blocks.add("海外仓卸柜或全部直送目的仓实际卸货后才能还空");
|
||||
else if(at==null) {code="RECORD_EXECUTION";label="确认实际还空";permission="platform:firstleg:terminal-drayage:empty-return:confirm";}
|
||||
// Return responsibility belongs to the work handoff, not the historical pickup carrier.
|
||||
}
|
||||
@@ -75,7 +102,7 @@ public class AppointmentWorkTerminalFacts implements OriginalFacts {
|
||||
var requirement=source.requirements().stream().filter(r->Objects.equals(r.requirementKey(),request.requirementKey())).findFirst()
|
||||
.orElseThrow(()->new ResponseStatusException(HttpStatus.CONFLICT,"直送需求与原柜来源不一致"));
|
||||
references.clear(); references.addAll(requirement.references());
|
||||
var row=direct.deliveries(id).stream().filter(d->("INBOUND:"+d.inboundId()).equals(request.requirementKey())).findFirst()
|
||||
var row=deliveries.stream().filter(d->("INBOUND:"+d.inboundId()).equals(request.requirementKey())).findFirst()
|
||||
.orElseThrow(()->new ResponseStatusException(HttpStatus.CONFLICT,"直送需求与实际装柜来源不一致"));
|
||||
at=row.actualAt();zone=row.timezone();businessNo=row.inboundCode();
|
||||
if(at==null && (node==null || node.pickedUpAt()==null)) blocks.add("实际提柜后才能记录直送卸货");
|
||||
|
||||
+23
-1
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class UnloadingEvidenceGuard implements EmptyReturnPrerequisite {
|
||||
@@ -16,11 +17,32 @@ public class UnloadingEvidenceGuard implements EmptyReturnPrerequisite {
|
||||
public LocalDateTime deadline(int hours){return instant().plus(Duration.ofHours(hours)).atZone(ZoneId.of(timezone)).toLocalDateTime();}
|
||||
}
|
||||
public DirectCompletion directCompletion(long id) {
|
||||
var rows=mapper.deliveries(id);
|
||||
return directCompletion(mapper.deliveries(id));
|
||||
}
|
||||
private DirectCompletion directCompletion(List<DirectDeliveryMapper.Delivery> rows) {
|
||||
if(rows.isEmpty() || rows.stream().anyMatch(row->row.actualAt()==null)) return null;
|
||||
return rows.stream().map(row->new DirectCompletion(row.actualAt(),row.timezone()))
|
||||
.max(java.util.Comparator.comparing(DirectCompletion::instant)).orElse(null);
|
||||
}
|
||||
public Map<Long,LocalDateTime> completedUnloading(List<Long> ids) {
|
||||
var result=new HashMap<Long,LocalDateTime>();
|
||||
if(ids.isEmpty()) return result;
|
||||
var pending=new ArrayList<Long>();
|
||||
for(var container:mapper.containers(ids)) {
|
||||
if(!"ACTIVE".equals(container.recordStatus())) continue;
|
||||
if(container.warehouseUnloadedAt()!=null) result.put(container.id(),container.warehouseUnloadedAt());
|
||||
else pending.add(container.id());
|
||||
}
|
||||
if(!pending.isEmpty()) {
|
||||
var deliveries=new HashMap<Long,List<DirectDeliveryMapper.Delivery>>();
|
||||
for(var row:mapper.deliveriesBatch(pending)) deliveries.computeIfAbsent(row.containerBookingId(),ignored->new ArrayList<>()).add(row.delivery());
|
||||
for(long id:pending) {
|
||||
var completed=directCompletion(deliveries.getOrDefault(id,List.of()));
|
||||
if(completed!=null) result.put(id,completed.localAt());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public LocalDateTime completedUnloading(long id) {
|
||||
var container=mapper.container(id);
|
||||
if(container==null || !"ACTIVE".equals(container.recordStatus())) return null;
|
||||
|
||||
Reference in New Issue
Block a user