This commit is contained in:
2026-08-21 17:05:52 +08:00
parent cd477fe40a
commit 8c280e19c4
18 changed files with 411 additions and 103 deletions
@@ -39,12 +39,8 @@ public class CarrierScheduleQueryScheduler {
this.repository = repository; this.service = service; this.integration = integration;
}
@Scheduled(cron = "${baiyue.carrier-schedule.scheduler.cron-morning:0 0 9 * * *}",
zone = "${baiyue.carrier-schedule.scheduler.zone:Asia/Shanghai}")
@Scheduled(cron = "${baiyue.carrier-schedule.scheduler.cron-noon:0 30 12 * * *}",
zone = "${baiyue.carrier-schedule.scheduler.zone:Asia/Shanghai}")
@Scheduled(cron = "${baiyue.carrier-schedule.scheduler.cron-afternoon:0 0 16 * * *}",
zone = "${baiyue.carrier-schedule.scheduler.zone:Asia/Shanghai}")
@Scheduled(fixedDelayString = "${baiyue.carrier-schedule.scheduler.scan-fixed-delay:PT1M}",
initialDelayString = "${baiyue.carrier-schedule.scheduler.scan-initial-delay:PT10S}")
public void poll() {
LocalDateTime triggerAt = LocalDateTime.ofInstant(clock.instant(), ZoneId.of(scheduleZone));
int batch = Math.max(1, Math.min(batchSize, 100));
@@ -65,6 +61,7 @@ public class CarrierScheduleQueryScheduler {
/** Executes only after this worker wins the database lease. */
void execute(CarrierScheduleQueryEntity candidate, LocalDateTime now) {
if (!repository.claimLease(candidate.getId(), workerId, now, now.plus(leaseDuration))) return;
if (service.deferMissedNormalSlot(candidate.getId(), now)) return;
try {
var response = integration.query(internalToken,
new QueryRequest(candidate.getCarrierCode(), candidate.getTrackingType(), candidate.getTrackingNo()));
@@ -53,6 +53,7 @@ import java.util.Set;
@Service
public class CarrierScheduleQueryService {
static final int MAX_SLOT_ATTEMPTS = 3;
private static final Set<String> TRACKING_TYPES = Set.of("BOOKING_NO", "BILL_OF_LADING", "CONTAINER_NO");
@Resource private CarrierScheduleQueryRepository repository;
@@ -217,34 +218,76 @@ public class CarrierScheduleQueryService {
return false;
}
@Transactional
public boolean deferMissedNormalSlot(long id, LocalDateTime scanAt) {
CarrierScheduleQueryEntity value = requiredLocked(id);
if ("STOPPED".equals(value.getStatus())) return false;
int attempts = value.getAttemptCount() == null ? 0 : value.getAttemptCount();
boolean staleRetry = attempts > 0 && value.getWindowStartedAt() != null
&& !scanAt.isBefore(CarrierScheduleQuerySlots.nextSlot(value.getWindowStartedAt()));
if (staleRetry) {
LocalDateTime currentSlot = CarrierScheduleQuerySlots.fixedSlotInCurrentMinute(scanAt);
if (currentSlot != null) {
value.setNextAllowedAt(currentSlot); value.setWindowStartedAt(currentSlot);
value.setAttemptCount(0); value.setConsecutiveFailures(0);
if (repository.updateState(value, value.getVersion(), 0L, "carrier-scheduler") != 1) throw concurrent();
return false;
}
} else if (!CarrierScheduleQuerySlots.isMissedNormalSlot(
value.getNextAllowedAt(), value.getAttemptCount(), scanAt)) return false;
value.setLeaseOwner(null); value.setLeaseUntil(null);
value.setNextAllowedAt(CarrierScheduleQuerySlots.nextSlot(scanAt));
value.setWindowStartedAt(value.getNextAllowedAt());
value.setAttemptCount(0); value.setConsecutiveFailures(0);
if (repository.updateState(value, value.getVersion(), 0L, "carrier-scheduler") != 1) throw concurrent();
return true;
}
@Transactional
public void completeScheduledAttempt(long id, String outcome, String errorCode, String errorMessage) {
CarrierScheduleQueryEntity value = requiredLocked(id);
insertAttempt(value, outcome, errorCode, errorMessage);
int slotAttemptNo = Math.min(MAX_SLOT_ATTEMPTS, increment(value.getAttemptCount()));
insertAttempt(value, slotAttemptNo, outcome, errorCode, errorMessage);
if ("STOPPED".equals(value.getStatus())) return;
CarrierScheduleQueryEntity before = copy(value);
LocalDateTime now = now();
value.setLeaseOwner(null); value.setLeaseUntil(null); value.setLastAttemptAt(now);
value.setLastAttemptStatus(outcome); value.setLastErrorCode(trim(errorCode));
value.setLastErrorMessage(trim(errorMessage));
boolean success = "SUCCESS".equals(outcome) || "NO_CHANGE".equals(outcome);
value.setStatus("MANUAL_REQUIRED".equals(outcome) || "PERMANENT_FAILURE".equals(outcome)
? "MANUAL_REQUIRED" : "READY");
value.setConsecutiveFailures(success ? 0 : increment(value.getConsecutiveFailures()));
value.setAttemptCount(increment(value.getAttemptCount()));
if (value.getWindowStartedAt() == null) value.setWindowStartedAt(now);
value.setNextAllowedAt(CarrierScheduleQuerySlots.nextSlot(now));
LocalDateTime nextFixedSlot = CarrierScheduleQuerySlots.nextSlot(now);
if ("RETRYABLE_FAILURE".equals(outcome)) {
value.setStatus("READY");
value.setConsecutiveFailures(slotAttemptNo);
LocalDateTime retryAt = now.plusMinutes(15L << Math.max(0, slotAttemptNo - 1));
if (slotAttemptNo < MAX_SLOT_ATTEMPTS && retryAt.isBefore(nextFixedSlot)) {
if (slotAttemptNo == 1 || value.getWindowStartedAt() == null) {
value.setWindowStartedAt(value.getNextAllowedAt());
}
value.setAttemptCount(slotAttemptNo); value.setNextAllowedAt(retryAt);
} else {
resetForNextFixedSlot(value, nextFixedSlot);
}
} else {
value.setStatus("MANUAL_REQUIRED".equals(outcome) || "PERMANENT_FAILURE".equals(outcome)
? "MANUAL_REQUIRED" : "READY");
resetForNextFixedSlot(value, nextFixedSlot);
}
if (repository.updateState(value, value.getVersion(), 0L, "carrier-scheduler") != 1) throw concurrent();
CarrierScheduleQueryEntity after = required(id);
addHistory(id, "PROVIDER_QUERY", before.getVersion(), after.getVersion(), outcome, before, after, 0L,
"carrier-scheduler");
}
private void insertAttempt(CarrierScheduleQueryEntity value, String outcome,
private static void resetForNextFixedSlot(CarrierScheduleQueryEntity value, LocalDateTime nextFixedSlot) {
value.setConsecutiveFailures(0); value.setAttemptCount(0);
value.setWindowStartedAt(nextFixedSlot); value.setNextAllowedAt(nextFixedSlot);
}
private void insertAttempt(CarrierScheduleQueryEntity value, int slotAttemptNo, String outcome,
String errorCode, String errorMessage) {
CarrierScheduleQueryAttemptEntity attempt = new CarrierScheduleQueryAttemptEntity();
attempt.setId(IdWorker.getId()); attempt.setQueryId(value.getId());
attempt.setWindowAttemptNo(increment(value.getAttemptCount()));
attempt.setWindowAttemptNo(slotAttemptNo);
attempt.setCarrierCode(value.getCarrierCode()); attempt.setAccessMode(value.getAccessMode());
attempt.setOfficialUrl(value.getOfficialUrl()); attempt.setAttemptStatus(outcome);
boolean manual = "MANUAL_REQUIRED".equals(outcome);
@@ -301,7 +344,8 @@ public class CarrierScheduleQueryService {
value.setStatus("READY"); value.setResultPortScheduleId(schedule.getId());
value.setLastSuccessAt(now); value.setSourceUpdatedAt(schedule.getObservedAt());
value.setExpiresAt(schedule.getExpiresAt()); value.setNextAllowedAt(CarrierScheduleQuerySlots.nextSlot(now));
value.setWindowStartedAt(now); value.setLastAttemptStatus("MANUAL_COMPLETED");
value.setWindowStartedAt(value.getNextAllowedAt()); value.setAttemptCount(0);
value.setConsecutiveFailures(0); value.setLastAttemptStatus("MANUAL_COMPLETED");
value.setManualReason(trim(request.reason())); value.setLastErrorCode(null); value.setLastErrorMessage(null);
if (repository.updateState(value, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
CarrierScheduleQueryEntity after = required(id);
@@ -17,4 +17,28 @@ public final class CarrierScheduleQuerySlots {
if (now.toLocalTime().isBefore(AFTERNOON)) return now.toLocalDate().atTime(AFTERNOON);
return now.toLocalDate().plusDays(1).atTime(MORNING);
}
public static boolean isFixedSlot(LocalDateTime value) {
if (value == null || value.getSecond() != 0 || value.getNano() != 0) return false;
return value.toLocalTime().equals(MORNING) || value.toLocalTime().equals(NOON)
|| value.toLocalTime().equals(AFTERNOON);
}
public static boolean isWithinFixedSlotMinute(LocalDateTime value) {
if (value == null) return false;
LocalTime minute = value.toLocalTime().withSecond(0).withNano(0);
return minute.equals(MORNING) || minute.equals(NOON) || minute.equals(AFTERNOON);
}
public static LocalDateTime fixedSlotInCurrentMinute(LocalDateTime value) {
return isWithinFixedSlotMinute(value) ? value.withSecond(0).withNano(0) : null;
}
/** A normal slot is executable only during its scheduled minute; technical retry times are unaffected. */
public static boolean isMissedNormalSlot(LocalDateTime nextAllowedAt, Integer attemptCount,
LocalDateTime scanAt) {
return (attemptCount == null || attemptCount == 0) && isFixedSlot(nextAllowedAt)
&& !isWithinFixedSlotMinute(scanAt)
&& !scanAt.isBefore(nextAllowedAt.plusMinutes(1));
}
}