fix(dispatch): enforce confirmed FBA appointment before scheduling

This commit is contained in:
2026-10-22 14:55:56 +08:00
parent 760bf8daa1
commit fc25507915
8 changed files with 200 additions and 11 deletions
@@ -33,7 +33,7 @@ public interface AppointmentWorkDispatchPort {
String departureBlockReason(String dispatchNo);
record ReceivingWindow(String batchKey,String batchNo,String customerId,String destinationKey,String confirmationNo,
java.time.Instant startAt,java.time.Instant endAt,String agreementKey,int version) { }
/** 预排不受限;提交审核、审核通过和开始拣货须有真实收货确认及可执行时间。 */
/** FBA选入前须已有正式时间确认;提交审核、审核通过和开始拣货继续复核运输安排。 */
String reviewBlockReason(String dispatchNo,java.time.Instant plannedDepartureAt,java.time.Instant estimatedArrivalAt);
List<ReceivingWindow> reviewEvidence(String dispatchNo);
void removeItem(long dispatchId,long itemId);
@@ -25,16 +25,25 @@ public class AppointmentDispatchAvailability {
&& a.members().stream().anyMatch(m->snapshot.batchKey().equals(m.batchKey()) && customer.equals(m.customerId()))).toList();
if(agreements.size()!=1)return 0;
var agreement=agreements.get(0);var details=agreement.details();
if(details==null || !"SUCCESS".equals(details.appointmentStatus()) || details.appointmentAt()==null || !validTime(details.appointmentAt(),agreement.timezone(),details.startOffset()))return 0;
if(details.appointmentEndAt()!=null && (!validTime(details.appointmentEndAt(),agreement.timezone(),details.endOffset()) || details.appointmentEndAt().isBefore(details.appointmentAt())))return 0;
if(details==null || !"SUCCESS".equals(details.appointmentStatus()) || details.appointmentAt()==null)return 0;
var start=confirmedInstant(details.appointmentAt(),agreement.timezone(),details.startOffset());
if(start==null)return 0;
if(details.appointmentEndAt()!=null) {
var end=confirmedInstant(details.appointmentEndAt(),agreement.timezone(),details.endOffset());
if(end==null || end.isBefore(start))return 0;
}
var members=agreement.members().stream().filter(m->snapshot.batchKey().equals(m.batchKey()) && customer.equals(m.customerId())).toList();
if(members.size()!=1)return 0;
var sources=members.get(0).sources().stream().filter(s->source.equals(s.sourceKey()) && destination.equals(s.destinationKey())).toList();
return sources.size()==1?Math.max(0,sources.get(0).packageCount()):0;
}
/** 夏令时歧义须有明确偏移;不能把当地时间按服务器时区猜测。 */
private boolean validTime(LocalDateTime at,String timezone,String offset) {
try {var offsets=ZoneId.of(timezone).getRules().getValidOffsets(at);return offset==null?offsets.size()==1:offsets.contains(ZoneOffset.of(offset));}
catch(DateTimeException|NullPointerException error){return false;}
private Instant confirmedInstant(LocalDateTime at,String timezone,String offset) {
try {
var offsets=ZoneId.of(timezone).getRules().getValidOffsets(at);
if(offsets.isEmpty() || offset==null && offsets.size()!=1)return null;
var selected=offset==null?offsets.get(0):ZoneOffset.of(offset);
return offsets.contains(selected)?at.toInstant(selected):null;
} catch(DateTimeException|NullPointerException error){return null;}
}
}