feat: add algorithm core for shadow observation

This commit is contained in:
zgw
2026-08-11 16:59:43 +08:00
parent 9480882752
commit 86fc240b84
99 changed files with 65211 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
package com.ruoyi.algorithm;
import com.ruoyi.algorithm.model.LaneState;
import com.ruoyi.algorithm.model.SpaceState;
import com.ruoyi.algorithm.model.SpaceStatus;
import com.ruoyi.algorithm.policy.SchedulingPolicy;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public final class LaneAnalyzer {
public LaneState analyze(List<SpaceState> laneSpaces, SchedulingPolicy policy) {
List<SpaceState> spaces = laneSpaces.stream()
.sorted(Comparator.comparingInt(SpaceState::depthOrder))
.toList();
boolean emptySeen = false;
boolean physicallyContinuous = true;
boolean invalidInventoryState = false;
int segmentCount = 0;
String currentSegmentKey = null;
String residentProject = null;
String guestProject = null;
for (SpaceState space : spaces) {
boolean occupied = space.occupied();
if (!occupied) {
emptySeen = true;
continue;
}
if (emptySeen) {
physicallyContinuous = false;
}
if (space.inventoryProjectConflict()
|| (space.status() == SpaceStatus.EMPTY && space.hasTray())) {
invalidInventoryState = true;
}
String segmentKey = space.projectNo() == null ? "__UNKNOWN__" : space.projectNo();
if (!segmentKey.equals(currentSegmentKey)) {
currentSegmentKey = segmentKey;
segmentCount++;
if (segmentCount == 1) {
residentProject = space.projectNo();
} else if (segmentCount == 2) {
guestProject = space.projectNo();
}
}
}
boolean corrupt = invalidInventoryState || !physicallyContinuous || segmentCount > 2;
Optional<SpaceState> nextSlot = corrupt
? Optional.empty()
: spaces.stream().filter(SpaceState::availableEmpty).findFirst();
int outermostOccupiedDepth = spaces.stream()
.filter(SpaceState::occupied)
.mapToInt(SpaceState::depthOrder)
.max()
.orElse(0);
Optional<SpaceState> outerSlot = spaces.stream()
.filter(SpaceState::availableEmpty)
.filter(space -> space.depthOrder() > outermostOccupiedDepth)
.findFirst();
return new LaneState(
spaces,
physicallyContinuous,
corrupt,
segmentCount,
Optional.ofNullable(residentProject),
Optional.ofNullable(guestProject),
nextSlot,
outerSlot
);
}
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.algorithm.inbound;
public enum BatchLaneMode {
OPEN,
SEALED
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.algorithm.inbound;
public record InboundAssignment(
String requestId,
String spaceId,
String laneCode,
int laneCapacity,
int depthOrder,
InboundLadderRung ladderRung,
InboundBorrowType borrowType,
String mixedIntoResidentProject,
double capacityScore,
double efficiencyScore,
double totalScore,
double fillRatePercent,
boolean batchLaneApplied,
boolean openedNewLane,
String reason
) {
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.algorithm.inbound;
public enum InboundBorrowType {
NONE,
CACHE_BORROWED,
RESERVE_BORROWED,
MIXED_IN
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.algorithm.inbound;
import com.ruoyi.algorithm.model.SpaceState;
import java.util.List;
public record InboundContext(
List<SpaceState> spaces,
InboundPolicy policy,
List<PendingOutbound> pendingOutbounds,
int occupiedCount
) {
public InboundContext {
spaces = spaces == null ? List.of() : List.copyOf(spaces);
policy = policy == null ? InboundPolicy.defaults() : policy;
pendingOutbounds = pendingOutbounds == null ? List.of() : List.copyOf(pendingOutbounds);
if (occupiedCount < 0) throw new IllegalArgumentException("occupiedCount cannot be negative");
}
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.algorithm.inbound;
public enum InboundLadderRung {
CONTINUE,
OPEN_GRADED,
OPEN_ADJACENT,
MIXED_GUEST,
POOL_BORROW,
COMMON_SINGLE,
COMMON_LANE_CONTINUE,
COMMON_LANE_OPEN
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.algorithm.inbound;
import java.util.Optional;
public record InboundPlan(Optional<InboundAssignment> assignment, Optional<InboundRejection> rejection) {
public InboundPlan {
assignment = assignment == null ? Optional.empty() : assignment;
rejection = rejection == null ? Optional.empty() : rejection;
if (assignment.isPresent() == rejection.isPresent()) {
throw new IllegalArgumentException("plan must contain exactly one assignment or rejection");
}
}
public static InboundPlan assigned(InboundAssignment assignment) {
return new InboundPlan(Optional.of(assignment), Optional.empty());
}
public static InboundPlan rejected(InboundRejection rejection) {
return new InboundPlan(Optional.empty(), Optional.of(rejection));
}
}

View File

@@ -0,0 +1,674 @@
package com.ruoyi.algorithm.inbound;
import com.ruoyi.algorithm.model.SpaceRole;
import com.ruoyi.algorithm.model.SpaceState;
import com.ruoyi.algorithm.model.SpaceStatus;
import com.ruoyi.algorithm.policy.SchedulingPolicy;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public final class InboundPlanner {
private static final Map<Integer, List<Integer>> ADJACENT_DEPTH_ORDER = Map.of(
2, List.of(3, 5),
3, List.of(5, 2),
5, List.of(3, 2)
);
public InboundPlan plan(InboundRequest request, InboundContext context) {
Objects.requireNonNull(request, "request");
Objects.requireNonNull(context, "context");
validateManualReservations(context);
List<Lane> lanes = buildLanes(context);
Optional<Candidate> selected = context.policy().isCommon(request.projectNo())
? chooseCommon(request, context, lanes)
: chooseProject(request, context, lanes);
if (selected.isPresent()) {
return InboundPlan.assigned(toAssignment(request, selected.orElseThrow()));
}
List<LadderTraceStep> trace = traceFor(request, context, lanes);
boolean sealedProjectRequest = request.batchLaneMode() == BatchLaneMode.SEALED
&& !context.policy().isCommon(request.projectNo());
InboundRejectionCode code = sealedProjectRequest
? InboundRejectionCode.NO_BATCH_COMPATIBLE_LANE
: InboundRejectionCode.NO_FEASIBLE_LOCATION;
String reason = sealedProjectRequest
? "No lane can continue the same project, product and lot, and no completely empty lane remains."
: "Inbound admission ladder exhausted every legal location; no feasible location remains.";
return InboundPlan.rejected(new InboundRejection(
code, reason, request.projectNo(), request.productId(), request.lotNo(), trace));
}
public PagedSession startPaged(InboundRequest request, InboundPolicy policy,
List<PendingOutbound> pendingOutbounds, int occupiedCount) {
return new PagedSession(request, policy, pendingOutbounds, occupiedCount);
}
public final class PagedSession {
private final InboundRequest request;
private final InboundPolicy policy;
private final List<PendingOutbound> pendingOutbounds;
private final int occupiedCount;
private final Map<String, Candidate> retained = new LinkedHashMap<>();
private final java.util.Set<String> missingManualLanes;
private long freeSinglePool;
private PagedSession(InboundRequest request, InboundPolicy policy,
List<PendingOutbound> pendingOutbounds, int occupiedCount) {
this.request = Objects.requireNonNull(request, "request");
this.policy = Objects.requireNonNull(policy, "policy");
this.pendingOutbounds = List.copyOf(pendingOutbounds == null ? List.of() : pendingOutbounds);
this.occupiedCount = occupiedCount;
this.missingManualLanes = policy.manualReservedLanes().stream()
.map(ManualLaneReservation::laneCode)
.collect(Collectors.toCollection(java.util.LinkedHashSet::new));
}
public void accept(List<SpaceState> pageSpaces) {
InboundContext context = new InboundContext(pageSpaces, policy, pendingOutbounds, occupiedCount);
List<Lane> lanes = buildLanes(context);
lanes.forEach(lane -> missingManualLanes.remove(lane.code()));
if (policy.isCommon(request.projectNo())) {
acceptCommon(context, lanes);
} else {
acceptProject(context, lanes);
}
}
public Optional<InboundAssignment> currentAssignment() {
return selected().map(candidate -> toAssignment(request, candidate));
}
public java.util.Set<String> retainedLaneCodes() {
return retained.values().stream().map(candidate -> candidate.lane().code())
.collect(Collectors.toUnmodifiableSet());
}
public InboundPlan finish() {
if (!missingManualLanes.isEmpty()) {
throw new IllegalArgumentException("manualReservedLanes references unknown lane "
+ missingManualLanes.iterator().next());
}
Optional<Candidate> candidate = selected();
if (candidate.isPresent()) return InboundPlan.assigned(toAssignment(request, candidate.orElseThrow()));
boolean sealed = request.batchLaneMode() == BatchLaneMode.SEALED
&& !policy.isCommon(request.projectNo());
InboundRejectionCode code = sealed ? InboundRejectionCode.NO_BATCH_COMPATIBLE_LANE
: InboundRejectionCode.NO_FEASIBLE_LOCATION;
String reason = sealed
? "No lane can continue the same project, product and lot, and no completely empty lane remains."
: "Inbound admission ladder exhausted every legal location; no feasible location remains.";
return InboundPlan.rejected(new InboundRejection(code, reason, request.projectNo(),
request.productId(), request.lotNo(), pagedTrace(sealed)));
}
private void acceptProject(InboundContext context, List<Lane> lanes) {
retain("CONTINUE", best(lanes.stream()
.filter(lane -> lane.usableForInbound() && !lane.occupied().isEmpty())
.map(lane -> continueCandidate(request, context, lane)).flatMap(Optional::stream).toList()),
candidateComparator());
int targetDepth = gradedTargetDepth(request, context);
retain("GRADED", best(emptyLaneCandidates(request, context, lanes, targetDepth,
InboundLadderRung.OPEN_GRADED)), candidateComparator());
for (int depth : ADJACENT_DEPTH_ORDER.getOrDefault(targetDepth, List.of())) {
retain("ADJACENT:" + depth, best(emptyLaneCandidates(request, context, lanes, depth,
InboundLadderRung.OPEN_ADJACENT)), candidateComparator());
}
if (request.batchLaneMode() != BatchLaneMode.SEALED && request.projectNo() != null
&& !policy.isIsolated(request.projectNo()) && !policy.isCommon(request.projectNo())
&& policy.allowsMixed(request.projectNo())) {
Comparator<Candidate> mixedOrder = Comparator.comparingInt(Candidate::remaining)
.thenComparing(Candidate::residentNewest).thenComparing(candidateComparator());
retain("MIXED", bestMixed(request, context, lanes), mixedOrder);
}
acceptPool(context, lanes);
}
private void acceptPool(InboundContext context, List<Lane> lanes) {
List<Lane> pool = lanes.stream().filter(lane -> lane.depth() == 1)
.filter(lane -> lane.spaces().stream().allMatch(space -> space.role() == SpaceRole.STORAGE))
.toList();
freeSinglePool += pool.stream().filter(Lane::isEmptyLane).count();
List<PoolTier> tiers = new ArrayList<>();
tiers.add(new PoolTier("FLEX", InboundBorrowType.NONE));
tiers.add(new PoolTier("CACHE", InboundBorrowType.CACHE_BORROWED));
if (occupiedCount >= policy.schedulingPolicy().capacityFirstOccupied()) {
tiers.add(new PoolTier("RESERVE", InboundBorrowType.RESERVE_BORROWED));
}
for (PoolTier tier : tiers) {
retain("POOL:" + tier.zone(), best(pool.stream().filter(Lane::usableForInbound)
.filter(Lane::isEmptyLane)
.filter(lane -> tier.zone().equalsIgnoreCase(lane.spaces().get(0).zoneCode()))
.filter(lane -> usableSlot(lane, lane.spaces().get(0), request, context))
.map(lane -> score(lane, lane.spaces().get(0), request, context,
InboundLadderRung.POOL_BORROW, tier.borrowType(), null, true)).toList()),
candidateComparator());
}
}
private void acceptCommon(InboundContext context, List<Lane> lanes) {
for (String zone : List.of("FLEX", "CACHE", "RESERVE")) {
retain("COMMON_SINGLE:" + zone, best(lanes.stream().filter(Lane::usableForInbound)
.filter(lane -> lane.depth() == 1 && lane.isEmptyLane())
.filter(lane -> zone.equalsIgnoreCase(lane.spaces().get(0).zoneCode()))
.filter(lane -> usableSlot(lane, lane.spaces().get(0), request, context))
.map(lane -> score(lane, lane.spaces().get(0), request, context,
InboundLadderRung.COMMON_SINGLE, InboundBorrowType.NONE, null, true)).toList()),
candidateComparator());
}
retain("COMMON_CONTINUE", best(lanes.stream().filter(Lane::usableForInbound)
.filter(lane -> lane.depth() == 2 && !lane.occupied().isEmpty())
.filter(lane -> lane.nextSlot().isPresent())
.filter(lane -> lane.occupied().stream().allMatch(space -> commonCompatible(space, request, context)))
.filter(lane -> usableSlot(lane, lane.nextSlot().orElseThrow(), request, context))
.map(lane -> score(lane, lane.nextSlot().orElseThrow(), request, context,
InboundLadderRung.COMMON_LANE_CONTINUE, InboundBorrowType.NONE, null, false)).toList()),
candidateComparator());
retain("COMMON_OPEN", best(emptyLaneCandidates(request, context, lanes, 2,
InboundLadderRung.COMMON_LANE_OPEN)), candidateComparator());
}
private Optional<Candidate> selected() {
if (policy.isCommon(request.projectNo())) {
for (String zone : List.of("FLEX", "CACHE", "RESERVE")) {
Candidate candidate = retained.get("COMMON_SINGLE:" + zone);
if (candidate != null) return Optional.of(candidate);
}
if (retained.containsKey("COMMON_CONTINUE")) return Optional.of(retained.get("COMMON_CONTINUE"));
return Optional.ofNullable(retained.get("COMMON_OPEN"));
}
if (retained.containsKey("CONTINUE")) return Optional.of(retained.get("CONTINUE"));
if (retained.containsKey("GRADED")) return Optional.of(retained.get("GRADED"));
int targetDepth = gradedTargetDepth(request,
new InboundContext(List.of(), policy, pendingOutbounds, occupiedCount));
if (targetDepth == 2) {
Optional<Candidate> pool = selectedPool();
if (pool.isPresent()) return pool;
}
for (int depth : ADJACENT_DEPTH_ORDER.getOrDefault(targetDepth, List.of())) {
Candidate candidate = retained.get("ADJACENT:" + depth);
if (candidate != null) return Optional.of(candidate);
}
if (retained.containsKey("MIXED")) return Optional.of(retained.get("MIXED"));
return selectedPool();
}
private Optional<Candidate> selectedPool() {
if (policy.commonSinglePoolReserve() > 0
&& freeSinglePool - 1 < policy.commonSinglePoolReserve()) return Optional.empty();
for (String zone : List.of("FLEX", "CACHE", "RESERVE")) {
Candidate candidate = retained.get("POOL:" + zone);
if (candidate != null) return Optional.of(candidate);
}
return Optional.empty();
}
private void retain(String key, Optional<Candidate> candidate, Comparator<Candidate> comparator) {
candidate.ifPresent(value -> retained.merge(key, value,
(current, proposed) -> comparator.compare(proposed, current) < 0 ? proposed : current));
}
private List<LadderTraceStep> pagedTrace(boolean sealed) {
if (policy.isCommon(request.projectNo())) {
return List.of(
new LadderTraceStep(InboundLadderRung.COMMON_SINGLE,
"No outbound-free and reservation-compatible FLEX, CACHE or RESERVE single slot."),
new LadderTraceStep(InboundLadderRung.COMMON_LANE_CONTINUE,
"No physically continuous outbound-free double lane contains only the same common product."),
new LadderTraceStep(InboundLadderRung.COMMON_LANE_OPEN,
"No completely empty outbound-free double lane is available."));
}
int targetDepth = gradedTargetDepth(request,
new InboundContext(List.of(), policy, pendingOutbounds, occupiedCount));
boolean guard = policy.commonSinglePoolReserve() > 0
&& freeSinglePool - 1 < policy.commonSinglePoolReserve();
return List.of(
new LadderTraceStep(InboundLadderRung.CONTINUE,
"No physically continuous outbound-free lane can legally continue this project/cohort."),
new LadderTraceStep(InboundLadderRung.OPEN_GRADED,
"No completely empty legal lane at graded depth " + targetDepth + "."),
new LadderTraceStep(InboundLadderRung.OPEN_ADJACENT,
"No completely empty legal lane in adjacent order "
+ ADJACENT_DEPTH_ORDER.getOrDefault(targetDepth, List.of()) + "."),
new LadderTraceStep(InboundLadderRung.MIXED_GUEST,
sealed ? "SEALED admission never mixes projects or lots."
: "No one-segment non-isolated resident lane without pending outbound can accept one outer guest."),
new LadderTraceStep(InboundLadderRung.POOL_BORROW,
guard ? "Single-pool common-parts watermark would be breached."
: "No legal FLEX/CACHE single slot, or high-water RESERVE slot, is available."));
}
}
private void validateManualReservations(InboundContext context) {
List<String> laneCodes = context.spaces().stream().map(SpaceState::laneCode).distinct().toList();
context.policy().manualReservedLanes().stream()
.filter(reservation -> !laneCodes.contains(reservation.laneCode()))
.findFirst()
.ifPresent(reservation -> {
throw new IllegalArgumentException(
"manualReservedLanes references unknown lane " + reservation.laneCode());
});
}
private Optional<Candidate> chooseProject(InboundRequest request, InboundContext context, List<Lane> lanes) {
Optional<Candidate> candidate = best(lanes.stream()
.filter(lane -> lane.usableForInbound() && !lane.occupied().isEmpty())
.map(lane -> continueCandidate(request, context, lane))
.flatMap(Optional::stream)
.toList());
if (candidate.isPresent()) return candidate;
int targetDepth = gradedTargetDepth(request, context);
candidate = best(emptyLaneCandidates(request, context, lanes, targetDepth, InboundLadderRung.OPEN_GRADED));
if (candidate.isPresent()) return candidate;
if (targetDepth == 2) {
candidate = bestPoolSingle(request, context, lanes).candidate();
if (candidate.isPresent()) return candidate;
}
for (int depth : ADJACENT_DEPTH_ORDER.getOrDefault(targetDepth, List.of())) {
candidate = best(emptyLaneCandidates(request, context, lanes, depth, InboundLadderRung.OPEN_ADJACENT));
if (candidate.isPresent()) return candidate;
}
if (request.batchLaneMode() != BatchLaneMode.SEALED
&& request.projectNo() != null
&& !context.policy().isIsolated(request.projectNo())
&& !context.policy().isCommon(request.projectNo())
&& context.policy().allowsMixed(request.projectNo())) {
candidate = bestMixed(request, context, lanes);
if (candidate.isPresent()) return candidate;
}
return bestPoolSingle(request, context, lanes).candidate();
}
private Optional<Candidate> chooseCommon(InboundRequest request, InboundContext context, List<Lane> lanes) {
for (String zone : List.of("FLEX", "CACHE", "RESERVE")) {
Optional<Candidate> single = best(lanes.stream()
.filter(Lane::usableForInbound)
.filter(lane -> lane.depth() == 1 && lane.isEmptyLane())
.filter(lane -> zone.equalsIgnoreCase(lane.spaces().get(0).zoneCode()))
.filter(lane -> usableSlot(lane, lane.spaces().get(0), request, context))
.map(lane -> score(lane, lane.spaces().get(0), request, context,
InboundLadderRung.COMMON_SINGLE, InboundBorrowType.NONE, null, true))
.toList());
if (single.isPresent()) return single;
}
Optional<Candidate> continuation = best(lanes.stream()
.filter(Lane::usableForInbound)
.filter(lane -> lane.depth() == 2 && !lane.occupied().isEmpty())
.filter(lane -> lane.nextSlot().isPresent())
.filter(lane -> lane.occupied().stream().allMatch(space -> commonCompatible(space, request, context)))
.filter(lane -> usableSlot(lane, lane.nextSlot().orElseThrow(), request, context))
.map(lane -> score(lane, lane.nextSlot().orElseThrow(), request, context,
InboundLadderRung.COMMON_LANE_CONTINUE, InboundBorrowType.NONE, null, false))
.toList());
if (continuation.isPresent()) return continuation;
return best(emptyLaneCandidates(request, context, lanes, 2, InboundLadderRung.COMMON_LANE_OPEN));
}
private boolean commonCompatible(SpaceState space, InboundRequest request, InboundContext context) {
return context.policy().isCommon(space.projectNo())
&& Objects.equals(space.productId(), request.productId());
}
private Optional<Candidate> continueCandidate(InboundRequest request, InboundContext context, Lane lane) {
if (lane.segments().isEmpty()) return Optional.empty();
Segment resident = lane.segments().get(0);
boolean residentMatch = lane.segments().size() == 1
&& Objects.equals(resident.projectNo(), request.projectNo());
boolean guestMatch = lane.segments().size() == 2
&& Objects.equals(lane.segments().get(1).projectNo(), request.projectNo());
if (!residentMatch && !guestMatch) return Optional.empty();
Optional<SpaceState> slot = guestMatch ? lane.outerSlot() : lane.nextSlot();
if (slot.isEmpty() || !usableSlot(lane, slot.orElseThrow(), request, context)) return Optional.empty();
if (guestMatch && (!context.policy().allowsMixed(request.projectNo())
|| !context.policy().allowsMixed(resident.projectNo()))) {
return Optional.empty();
}
if (request.batchLaneMode() == BatchLaneMode.SEALED && lane.occupied().stream().anyMatch(space ->
!Objects.equals(space.projectNo(), request.projectNo())
|| !Objects.equals(space.productId(), request.productId())
|| !Objects.equals(space.lotNo(), request.lotNo()))) {
return Optional.empty();
}
return Optional.of(score(lane, slot.orElseThrow(), request, context, InboundLadderRung.CONTINUE,
guestMatch ? InboundBorrowType.MIXED_IN : InboundBorrowType.NONE,
guestMatch ? resident.projectNo() : null, false));
}
private List<Candidate> emptyLaneCandidates(InboundRequest request, InboundContext context,
List<Lane> lanes, int depth, InboundLadderRung rung) {
return lanes.stream()
.filter(Lane::usableForInbound)
.filter(lane -> lane.depth() == depth && lane.isEmptyLane())
.filter(lane -> lane.spaces().stream().allMatch(space -> space.role() == SpaceRole.STORAGE))
.filter(lane -> usableSlot(lane, lane.spaces().get(0), request, context))
.map(lane -> score(lane, lane.spaces().get(0), request, context, rung,
InboundBorrowType.NONE, null, true))
.toList();
}
private PoolChoice bestPoolSingle(InboundRequest request, InboundContext context, List<Lane> lanes) {
List<Lane> pool = lanes.stream()
.filter(lane -> lane.depth() == 1)
.filter(lane -> lane.spaces().stream().allMatch(space -> space.role() == SpaceRole.STORAGE))
.toList();
long freeTotal = pool.stream().filter(Lane::isEmptyLane).count();
if (context.policy().commonSinglePoolReserve() > 0
&& freeTotal - 1 < context.policy().commonSinglePoolReserve()) {
return new PoolChoice(Optional.empty(), true);
}
List<PoolTier> tiers = new ArrayList<>();
tiers.add(new PoolTier("FLEX", InboundBorrowType.NONE));
tiers.add(new PoolTier("CACHE", InboundBorrowType.CACHE_BORROWED));
if (context.occupiedCount() >= context.policy().schedulingPolicy().capacityFirstOccupied()) {
tiers.add(new PoolTier("RESERVE", InboundBorrowType.RESERVE_BORROWED));
}
for (PoolTier tier : tiers) {
Optional<Candidate> found = best(pool.stream()
.filter(Lane::usableForInbound)
.filter(Lane::isEmptyLane)
.filter(lane -> tier.zone().equalsIgnoreCase(lane.spaces().get(0).zoneCode()))
.filter(lane -> usableSlot(lane, lane.spaces().get(0), request, context))
.map(lane -> score(lane, lane.spaces().get(0), request, context,
InboundLadderRung.POOL_BORROW, tier.borrowType(), null, true))
.toList());
if (found.isPresent()) return new PoolChoice(found, false);
}
return new PoolChoice(Optional.empty(), false);
}
private Optional<Candidate> bestMixed(InboundRequest request, InboundContext context, List<Lane> lanes) {
Comparator<Candidate> mixedOrder = Comparator.comparingInt(Candidate::remaining)
.thenComparing(Candidate::residentNewest)
.thenComparing(candidateComparator());
return lanes.stream()
.filter(Lane::usableForInbound)
.filter(lane -> lane.depth() >= 2 && lane.segments().size() == 1 && lane.outerSlot().isPresent())
.filter(lane -> lane.segments().get(0).projectNo() != null)
.filter(lane -> !Objects.equals(lane.segments().get(0).projectNo(), request.projectNo()))
.filter(lane -> !context.policy().isIsolated(lane.segments().get(0).projectNo()))
.filter(lane -> !context.policy().isCommon(lane.segments().get(0).projectNo()))
.filter(lane -> context.policy().allowsMixed(request.projectNo()))
.filter(lane -> context.policy().allowsMixed(lane.segments().get(0).projectNo()))
.filter(lane -> usableSlot(lane, lane.outerSlot().orElseThrow(), request, context))
.map(lane -> score(lane, lane.outerSlot().orElseThrow(), request, context,
InboundLadderRung.MIXED_GUEST, InboundBorrowType.MIXED_IN,
lane.segments().get(0).projectNo(), false))
.min(mixedOrder);
}
private List<LadderTraceStep> traceFor(InboundRequest request, InboundContext context, List<Lane> lanes) {
if (context.policy().isCommon(request.projectNo())) {
return List.of(
new LadderTraceStep(InboundLadderRung.COMMON_SINGLE,
"No outbound-free and reservation-compatible FLEX, CACHE or RESERVE single slot."),
new LadderTraceStep(InboundLadderRung.COMMON_LANE_CONTINUE,
"No physically continuous outbound-free double lane contains only the same common product."),
new LadderTraceStep(InboundLadderRung.COMMON_LANE_OPEN,
"No completely empty outbound-free double lane is available."));
}
int targetDepth = gradedTargetDepth(request, context);
PoolChoice pool = bestPoolSingle(request, context, lanes);
String poolReason = pool.guardBlocked()
? "Single-pool common-parts watermark would be breached."
: "No legal FLEX/CACHE single slot, or high-water RESERVE slot, is available.";
return List.of(
new LadderTraceStep(InboundLadderRung.CONTINUE,
"No physically continuous outbound-free lane can legally continue this project/cohort."),
new LadderTraceStep(InboundLadderRung.OPEN_GRADED,
"No completely empty legal lane at graded depth " + targetDepth + "."),
new LadderTraceStep(InboundLadderRung.OPEN_ADJACENT,
"No completely empty legal lane in adjacent order " + ADJACENT_DEPTH_ORDER.getOrDefault(targetDepth, List.of()) + "."),
new LadderTraceStep(InboundLadderRung.MIXED_GUEST,
request.batchLaneMode() == BatchLaneMode.SEALED
? "SEALED admission never mixes projects or lots."
: "No one-segment non-isolated resident lane without pending outbound can accept one outer guest."),
new LadderTraceStep(InboundLadderRung.POOL_BORROW, poolReason));
}
private int gradedTargetDepth(InboundRequest request, InboundContext context) {
if (request.expectedProjectPallets() != null) {
if (request.expectedProjectPallets() <= 2) return 2;
if (request.expectedProjectPallets() == 3) return 3;
return 5;
}
long current = context.spaces().stream()
.filter(SpaceState::occupied)
.filter(space -> !space.outboundTaskActive())
.filter(space -> Objects.equals(space.projectNo(), request.projectNo()))
.count();
return current <= 1 ? 2 : 5;
}
private boolean pendingHitsPallet(InboundContext context, SpaceState pallet) {
return context.pendingOutbounds().stream().anyMatch(pending -> {
boolean exactTray = pending.trayNo() != null && !pending.trayNo().isBlank()
&& Objects.equals(pending.trayNo(), pallet.trayNo());
boolean resolvedLane = pending.hasResolvedLane()
&& Objects.equals(pending.sourceLaneCode(), pallet.laneCode());
return exactTray || resolvedLane;
});
}
private boolean usableSlot(Lane lane, SpaceState space, InboundRequest request, InboundContext context) {
if (!lane.usableForInbound() || !space.availableEmpty() || space.role() != SpaceRole.STORAGE) return false;
if (request.targetFloor() != null && space.floorNo() != request.targetFloor()) return false;
if (request.targetFloor() == null && !request.allowedFloors().isEmpty()
&& !request.allowedFloors().contains(space.floorNo())) return false;
return !reservationBlocks(lane.code(), request.projectNo(), context.policy());
}
private boolean reservationBlocks(String laneCode, String projectNo, InboundPolicy policy) {
Optional<ManualLaneReservation> reservation = policy.manualReservedLanes().stream()
.filter(item -> item.laneCode().equals(laneCode))
.findFirst();
if (reservation.isEmpty()) return false;
if (reservation.orElseThrow().projectNo() != null) {
return !Objects.equals(reservation.orElseThrow().projectNo(), projectNo);
}
return !policy.isIsolated(projectNo);
}
private Candidate score(Lane lane, SpaceState space, InboundRequest request, InboundContext context,
InboundLadderRung rung, InboundBorrowType borrowType,
String mixedIntoResidentProject, boolean openedNewLane) {
long sameProject = lane.occupied().stream()
.filter(item -> request.projectNo() != null && Objects.equals(item.projectNo(), request.projectNo()))
.count();
double fillRate = lane.spaces().isEmpty() ? 0 : (double) lane.occupied().size() / lane.spaces().size();
double codingProgress = (space.laneCapacity() - space.groupCoding() + 1.0) / Math.max(1, space.laneCapacity());
double zoneBonus = "MAIN".equalsIgnoreCase(space.zoneCode()) ? 8
: "FLEX".equalsIgnoreCase(space.zoneCode()) ? 5 : 0;
double capacityScore = clamp(20 + fillRate * 52 + (sameProject > 0 ? 20 : 0)
+ codingProgress * 15 + zoneBonus);
int entryFloor = Math.max(1, Math.min(8, request.entryFloor() == null ? 1 : request.entryFloor()));
int aisleDistance = Math.min(Math.abs(space.aisleNo() - 6), Math.abs(space.aisleNo() - 10));
double efficiencyScore = clamp(105 - Math.abs(space.floorNo() - entryFloor) * 12
- aisleDistance * 3 - numericRow(space.rowCode()) * 0.7);
SchedulingPolicy scheduling = context.policy().schedulingPolicy();
double capacityWeight = scheduling.capacityWeight();
double efficiencyWeight = scheduling.efficiencyWeight();
if (context.occupiedCount() >= scheduling.capacityFirstOccupied()) {
capacityWeight = 0.8;
efficiencyWeight = 0.2;
}
double sum = capacityWeight + efficiencyWeight;
if (sum == 0) {
capacityWeight = efficiencyWeight = 0.5;
sum = 1;
}
double totalScore = capacityScore * capacityWeight / sum + efficiencyScore * efficiencyWeight / sum;
Instant residentNewest = lane.occupied().stream()
.map(SpaceState::inboundAt)
.filter(Objects::nonNull)
.max(Comparator.naturalOrder())
.orElse(Instant.MAX);
if (lane.occupied().stream().anyMatch(item -> item.inboundAt() == null)) residentNewest = Instant.MAX;
return new Candidate(space, lane, rung, borrowType, mixedIntoResidentProject, openedNewLane,
round1(capacityScore), round1(efficiencyScore), round1(totalScore), round1(fillRate * 100),
lane.spaces().size() - lane.occupied().size(), residentNewest);
}
private Optional<Candidate> best(List<Candidate> candidates) {
return candidates.stream().min(candidateComparator());
}
private Comparator<Candidate> candidateComparator() {
return Comparator.comparingDouble(Candidate::totalScore).reversed()
.thenComparing(Comparator.comparingDouble(Candidate::capacityScore).reversed())
.thenComparing(candidate -> candidate.space().spaceId());
}
private InboundAssignment toAssignment(InboundRequest request, Candidate candidate) {
SpaceState space = candidate.space();
String reason = "Selected the innermost legal slot " + space.spaceId() + " in lane "
+ candidate.lane().code() + " at rung " + candidate.rung()
+ "; fill=" + candidate.fillRatePercent() + "%, capacity=" + candidate.capacityScore()
+ ", efficiency=" + candidate.efficiencyScore() + ", total=" + candidate.totalScore() + ".";
return new InboundAssignment(request.requestId(), space.spaceId(), candidate.lane().code(),
space.laneCapacity(), space.depthOrder(), candidate.rung(), candidate.borrowType(),
candidate.mixedIntoResidentProject(), candidate.capacityScore(), candidate.efficiencyScore(),
candidate.totalScore(), candidate.fillRatePercent(), request.batchLaneMode() == BatchLaneMode.SEALED,
candidate.openedNewLane(), reason);
}
private List<Lane> buildLanes(InboundContext context) {
Map<String, List<SpaceState>> grouped = context.spaces().stream().collect(Collectors.groupingBy(
SpaceState::laneCode, LinkedHashMap::new, Collectors.toList()));
return grouped.entrySet().stream().map(entry -> analyzeLane(entry.getKey(), entry.getValue(), context))
.toList();
}
private Lane analyzeLane(String code, List<SpaceState> input, InboundContext context) {
List<SpaceState> spaces = input.stream()
.sorted(Comparator.comparingInt(SpaceState::depthOrder).thenComparing(SpaceState::spaceId))
.toList();
List<SpaceState> occupied = spaces.stream().filter(SpaceState::occupied).toList();
boolean gapSeen = false;
boolean continuous = true;
boolean invalid = false;
for (SpaceState space : spaces) {
if (!space.occupied()) {
gapSeen = true;
} else if (gapSeen) {
continuous = false;
}
if (space.inventoryProjectConflict()
|| (space.status() == SpaceStatus.EMPTY && space.hasTray())) invalid = true;
}
List<Segment> segments = new ArrayList<>();
for (SpaceState space : occupied) {
String key = segmentKey(space, context.policy());
if (segments.isEmpty() || !segments.get(segments.size() - 1).key().equals(key)) {
segments.add(new Segment(key, space.projectNo(), space.productId()));
}
}
int depth = spaces.stream().mapToInt(SpaceState::laneCapacity).max().orElse(0);
boolean completeTopology = spaces.size() == depth;
for (int index = 0; index < spaces.size(); index++) {
SpaceState space = spaces.get(index);
if (space.depthOrder() != index + 1 || space.laneCapacity() != depth) {
completeTopology = false;
break;
}
}
boolean corrupt = invalid || !continuous || !completeTopology || segments.size() > 2;
int maxOccupiedDepth = occupied.stream().mapToInt(SpaceState::depthOrder).max().orElse(0);
Optional<SpaceState> immediateOuterSlot = corrupt ? Optional.empty()
: spaces.stream().filter(SpaceState::availableEmpty)
.filter(space -> space.depthOrder() == maxOccupiedDepth + 1).findFirst();
boolean emptyLane = occupied.isEmpty() && spaces.stream().allMatch(SpaceState::availableEmpty);
boolean outboundBlocked = spaces.stream().anyMatch(SpaceState::outboundTaskActive)
|| occupied.stream().anyMatch(space -> pendingHitsPallet(context, space));
return new Lane(code, depth, spaces, occupied, segments, corrupt, outboundBlocked,
emptyLane, immediateOuterSlot, immediateOuterSlot);
}
private String segmentKey(SpaceState space, InboundPolicy policy) {
if (space.projectNo() == null) return "P|__UNKNOWN__";
if (policy.isCommon(space.projectNo())) {
return "C|" + space.projectNo() + "|" + Objects.toString(space.productId(), "");
}
return "P|" + space.projectNo();
}
private double numericRow(String rowCode) {
try {
return Double.parseDouble(rowCode);
} catch (RuntimeException ignored) {
return 0;
}
}
private double clamp(double value) {
return Math.max(0, Math.min(100, value));
}
private double round1(double value) {
return Math.round(value * 10.0) / 10.0;
}
private record Segment(String key, String projectNo, String productId) {
}
private record Lane(
String code,
int depth,
List<SpaceState> spaces,
List<SpaceState> occupied,
List<Segment> segments,
boolean corrupt,
boolean outboundBlocked,
boolean isEmptyLane,
Optional<SpaceState> nextSlot,
Optional<SpaceState> outerSlot
) {
private boolean usableForInbound() {
return !corrupt && !outboundBlocked;
}
}
private record Candidate(
SpaceState space,
Lane lane,
InboundLadderRung rung,
InboundBorrowType borrowType,
String mixedIntoResidentProject,
boolean openedNewLane,
double capacityScore,
double efficiencyScore,
double totalScore,
double fillRatePercent,
int remaining,
Instant residentNewest
) {
}
private record PoolTier(String zone, InboundBorrowType borrowType) {
}
private record PoolChoice(Optional<Candidate> candidate, boolean guardBlocked) {
}
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.algorithm.inbound;
import com.ruoyi.algorithm.policy.SchedulingPolicy;
import java.util.List;
import java.util.Set;
public record InboundPolicy(
SchedulingPolicy schedulingPolicy,
Set<String> isolatedProjects,
Set<String> commonProjectNos,
List<ManualLaneReservation> manualReservedLanes,
int commonSinglePoolReserve,
Set<String> mixedDeniedProjects
) {
public InboundPolicy {
schedulingPolicy = schedulingPolicy == null ? SchedulingPolicy.ft546Defaults() : schedulingPolicy;
isolatedProjects = isolatedProjects == null ? Set.of() : Set.copyOf(isolatedProjects);
commonProjectNos = commonProjectNos == null ? Set.of("COMMON") : Set.copyOf(commonProjectNos);
manualReservedLanes = manualReservedLanes == null ? List.of() : List.copyOf(manualReservedLanes);
mixedDeniedProjects = mixedDeniedProjects == null ? Set.of() : Set.copyOf(mixedDeniedProjects);
if (commonSinglePoolReserve < 0) throw new IllegalArgumentException("commonSinglePoolReserve cannot be negative");
long distinctLanes = manualReservedLanes.stream().map(ManualLaneReservation::laneCode).distinct().count();
if (distinctLanes != manualReservedLanes.size()) {
throw new IllegalArgumentException("manualReservedLanes contains a duplicate lane");
}
}
public InboundPolicy(SchedulingPolicy schedulingPolicy, Set<String> isolatedProjects,
Set<String> commonProjectNos, List<ManualLaneReservation> manualReservedLanes,
int commonSinglePoolReserve) {
this(schedulingPolicy, isolatedProjects, commonProjectNos, manualReservedLanes,
commonSinglePoolReserve, Set.of());
}
public static InboundPolicy defaults() {
return new InboundPolicy(SchedulingPolicy.ft546Defaults(), Set.of(), Set.of("COMMON"), List.of(), 0);
}
public boolean isIsolated(String projectNo) {
return projectNo != null && isolatedProjects.contains(projectNo);
}
public boolean isCommon(String projectNo) {
return projectNo != null && commonProjectNos.contains(projectNo);
}
public boolean allowsMixed(String projectNo) {
return projectNo != null && !mixedDeniedProjects.contains(projectNo);
}
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.algorithm.inbound;
import java.util.List;
public record InboundRejection(
InboundRejectionCode code,
String reason,
String projectNo,
String productId,
String lotNo,
List<LadderTraceStep> ladderTrace
) {
public InboundRejection {
ladderTrace = List.copyOf(ladderTrace);
}
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.algorithm.inbound;
public enum InboundRejectionCode {
NO_FEASIBLE_LOCATION,
NO_BATCH_COMPATIBLE_LANE
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.algorithm.inbound;
import java.util.Set;
public record InboundRequest(
String requestId,
String trayNo,
String projectNo,
String productId,
String lotNo,
Integer expectedProjectPallets,
Integer entryFloor,
Integer targetFloor,
Set<Integer> allowedFloors,
BatchLaneMode batchLaneMode
) {
public InboundRequest {
if (requestId == null || requestId.isBlank()) throw new IllegalArgumentException("requestId is required");
if (trayNo == null || trayNo.isBlank()) throw new IllegalArgumentException("trayNo is required");
if (expectedProjectPallets != null && expectedProjectPallets <= 0) {
throw new IllegalArgumentException("expectedProjectPallets must be positive");
}
allowedFloors = allowedFloors == null ? Set.of() : Set.copyOf(allowedFloors);
batchLaneMode = batchLaneMode == null ? BatchLaneMode.OPEN : batchLaneMode;
}
}

View File

@@ -0,0 +1,4 @@
package com.ruoyi.algorithm.inbound;
public record LadderTraceStep(InboundLadderRung rung, String reason) {
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.algorithm.inbound;
public record ManualLaneReservation(String laneCode, String projectNo) {
public ManualLaneReservation {
if (laneCode == null || laneCode.isBlank()) {
throw new IllegalArgumentException("laneCode is required");
}
}
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.algorithm.inbound;
public record PendingOutbound(String projectNo, String productId, String trayNo,
String sourceLaneCode, String sourceResolution) {
public PendingOutbound(String projectNo, String productId, String trayNo) {
this(projectNo, productId, trayNo, null, "UNRESOLVED");
}
public boolean hasResolvedLane() {
if (sourceLaneCode == null || sourceLaneCode.isBlank()
|| sourceResolution == null || sourceResolution.isBlank()) {
return false;
}
return !"UNRESOLVED".equalsIgnoreCase(sourceResolution)
&& !"AMBIGUOUS_TRAY".equalsIgnoreCase(sourceResolution);
}
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.algorithm.integration;
import com.ruoyi.algorithm.inbound.InboundPlanner;
import com.ruoyi.algorithm.outbound.OutboundPlanner;
import com.ruoyi.algorithm.repository.JdbcWarehouseRepository;
import com.ruoyi.algorithm.repository.RepositoryDialect;
import com.ruoyi.algorithm.repository.WarehouseRepository;
import com.ruoyi.algorithm.service.WarehouseApplicationService;
import com.ruoyi.algorithm.tidy.TidyPlanner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@EnableConfigurationProperties(AlgorithmProperties.class)
public class AlgorithmConfiguration {
@Bean
WarehouseRepository algorithmWarehouseRepository(DataSource dataSource, AlgorithmProperties properties,
PlatformTransactionManager transactionManager) {
if (transactionManager instanceof DataSourceTransactionManager dataSourceTransactionManager
&& dataSourceTransactionManager.getDataSource() != dataSource) {
throw new IllegalStateException("Algorithm repository and WMS must use the same DataSource");
}
return new JdbcWarehouseRepository(dataSource, RepositoryDialect.sqlServer(),
properties.isDatabaseWritesEnabled(), transactionManager);
}
@Bean
WarehouseApplicationService algorithmApplicationService(WarehouseRepository algorithmWarehouseRepository,
AlgorithmProperties properties) {
return new WarehouseApplicationService(algorithmWarehouseRepository, new InboundPlanner(),
new OutboundPlanner(), new TidyPlanner(), properties.getPageSize());
}
@Bean
SchedulingFacade schedulingFacade(WarehouseApplicationService algorithmApplicationService,
AlgorithmProperties properties) {
return new SchedulingFacade(algorithmApplicationService, properties);
}
}

View File

@@ -0,0 +1,74 @@
package com.ruoyi.algorithm.integration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "fortucky.algorithm")
public class AlgorithmProperties {
private SchedulingMode mode = SchedulingMode.SHADOW;
private String warehouseId = "01";
private int pageSize = 100;
private boolean databaseWritesEnabled = true;
private boolean legacyFallbackOnTechnicalError;
private long tidyAutoScanMs = 600_000L;
private long tidyReconcileMs = 5_000L;
private int tidyClaimLeaseSeconds = 60;
private int tidyManualMaxConcurrency = 3;
private String tidyWcsSnapshotUrl = "http://localhost:8081/wcs2Wms/api/tidyVehicleSnapshot";
private String replayRunId = "";
private String replayDatabase = "";
public SchedulingMode getMode() {
return mode;
}
public void setMode(SchedulingMode mode) {
this.mode = mode;
}
public String getWarehouseId() {
return warehouseId;
}
public void setWarehouseId(String warehouseId) {
this.warehouseId = warehouseId;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public boolean isDatabaseWritesEnabled() {
return databaseWritesEnabled;
}
public void setDatabaseWritesEnabled(boolean databaseWritesEnabled) {
this.databaseWritesEnabled = databaseWritesEnabled;
}
public boolean isLegacyFallbackOnTechnicalError() {
return legacyFallbackOnTechnicalError;
}
public void setLegacyFallbackOnTechnicalError(boolean legacyFallbackOnTechnicalError) {
this.legacyFallbackOnTechnicalError = legacyFallbackOnTechnicalError;
}
public long getTidyAutoScanMs() { return tidyAutoScanMs; }
public void setTidyAutoScanMs(long value) { this.tidyAutoScanMs = value; }
public long getTidyReconcileMs() { return tidyReconcileMs; }
public void setTidyReconcileMs(long value) { this.tidyReconcileMs = value; }
public int getTidyClaimLeaseSeconds() { return tidyClaimLeaseSeconds; }
public void setTidyClaimLeaseSeconds(int value) { this.tidyClaimLeaseSeconds = value; }
public int getTidyManualMaxConcurrency() { return tidyManualMaxConcurrency; }
public void setTidyManualMaxConcurrency(int value) { this.tidyManualMaxConcurrency = value; }
public String getTidyWcsSnapshotUrl() { return tidyWcsSnapshotUrl; }
public void setTidyWcsSnapshotUrl(String value) { this.tidyWcsSnapshotUrl = value; }
public String getReplayRunId() { return replayRunId; }
public void setReplayRunId(String value) { this.replayRunId = value; }
public String getReplayDatabase() { return replayDatabase; }
public void setReplayDatabase(String value) { this.replayDatabase = value; }
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.algorithm.integration;
public enum SchedulingDecisionStatus {
READY,
WAITING,
REJECTED,
TECHNICAL_ERROR
}

View File

@@ -0,0 +1,82 @@
package com.ruoyi.algorithm.integration;
import com.ruoyi.algorithm.inbound.InboundRequest;
import com.ruoyi.algorithm.outbound.OutboundPlan;
import com.ruoyi.algorithm.outbound.OutboundRequest;
import com.ruoyi.algorithm.repository.DeepWorkflowMutationResult;
import com.ruoyi.algorithm.repository.ReservationResult;
import com.ruoyi.algorithm.service.InboundPlanningResult;
import com.ruoyi.algorithm.service.InboundReservationOutcome;
import com.ruoyi.algorithm.service.WarehouseApplicationService;
import com.ruoyi.algorithm.tidy.TidyPlan;
import java.time.Instant;
import java.util.Objects;
public final class SchedulingFacade {
private final WarehouseApplicationService applicationService;
private final AlgorithmProperties properties;
private final SchedulingRoutingPolicy routingPolicy;
public SchedulingFacade(WarehouseApplicationService applicationService, AlgorithmProperties properties) {
this.applicationService = Objects.requireNonNull(applicationService, "applicationService");
this.properties = Objects.requireNonNull(properties, "properties");
this.routingPolicy = new SchedulingRoutingPolicy(properties.getMode(),
properties.isLegacyFallbackOnTechnicalError());
}
public SchedulingRoutingPolicy routingPolicy() {
return routingPolicy;
}
public String defaultWarehouseId() {
return properties.getWarehouseId();
}
public InboundPlanningResult planInbound(String warehouseId, InboundRequest request) {
requireAlgorithmMode();
return applicationService.planInbound(warehouseId, request);
}
public InboundReservationOutcome reserveInbound(String reservationId, String warehouseId,
String projectNo, InboundPlanningResult plan) {
requireActiveMode();
return applicationService.reserveInbound(reservationId, warehouseId, projectNo, plan);
}
public OutboundPlan planOutbound(String warehouseId, OutboundRequest request) {
requireAlgorithmMode();
return applicationService.planOutbound(warehouseId, request);
}
public TidyPlan planTidy(String warehouseId, Instant now) {
requireAlgorithmMode();
return applicationService.planTidy(warehouseId, now);
}
public ReservationResult releaseInboundReservation(String warehouseId, String reservationId) {
return applicationService.releaseReservation(warehouseId, reservationId);
}
public DeepWorkflowMutationResult acquireDeepWorkflowLock(String warehouseId, String requestId, int floorNo) {
requireActiveMode();
return applicationService.acquireDeepWorkflowLock(warehouseId, requestId, floorNo);
}
public DeepWorkflowMutationResult releaseDeepWorkflowLock(String warehouseId, String requestId, int floorNo) {
return applicationService.releaseDeepWorkflowLock(warehouseId, requestId, floorNo);
}
private void requireAlgorithmMode() {
if (!routingPolicy.runAlgorithm()) {
throw new IllegalStateException("Java scheduling is disabled in LEGACY mode");
}
}
private void requireActiveMode() {
if (!routingPolicy.algorithmOwnsDecision()) {
throw new IllegalStateException("Algorithm writes require ACTIVE mode");
}
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.integration;
public enum SchedulingMode {
LEGACY,
SHADOW,
ACTIVE
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.algorithm.integration;
import java.util.Objects;
public record SchedulingRoutingPolicy(SchedulingMode mode, boolean legacyFallbackOnTechnicalError) {
public SchedulingRoutingPolicy {
mode = Objects.requireNonNull(mode, "mode");
}
public boolean runAlgorithm() {
return mode != SchedulingMode.LEGACY;
}
public boolean algorithmOwnsDecision() {
return mode == SchedulingMode.ACTIVE;
}
public boolean runLegacy(SchedulingDecisionStatus status) {
Objects.requireNonNull(status, "status");
return switch (mode) {
case LEGACY, SHADOW -> true;
case ACTIVE -> status == SchedulingDecisionStatus.TECHNICAL_ERROR
&& legacyFallbackOnTechnicalError;
};
}
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.algorithm.model;
import java.util.List;
import java.util.Optional;
public record LaneState(
List<SpaceState> spaces,
boolean physicallyContinuous,
boolean corrupt,
int segmentCount,
Optional<String> residentProjectNo,
Optional<String> guestProjectNo,
Optional<SpaceState> nextSlot,
Optional<SpaceState> outerSlot
) {
public LaneState {
spaces = List.copyOf(spaces);
}
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.algorithm.model;
public enum SpaceRole {
STORAGE,
RELOCATION_BUFFER,
OUTBOUND_CACHE,
PROTECTED_CORE
}

View File

@@ -0,0 +1,54 @@
package com.ruoyi.algorithm.model;
import java.time.Instant;
public record SpaceState(
String spaceId,
String zoneCode,
String laneCode,
int depthOrder,
int laneCapacity,
int floorNo,
int aisleNo,
int groupCoding,
String rowCode,
SpaceRole role,
SpaceStatus status,
boolean outboundTaskActive,
boolean inventoryProjectConflict,
String trayNo,
String projectNo,
String productId,
String lotNo,
Instant inboundAt,
Instant outboundAt,
boolean reserved,
String rowVersion,
boolean physicalStockPresent
) {
public SpaceState(String spaceId, String zoneCode, String laneCode, int depthOrder, int laneCapacity,
int floorNo, int aisleNo, int groupCoding, String rowCode, SpaceRole role,
SpaceStatus status, boolean outboundTaskActive, boolean inventoryProjectConflict,
String trayNo, String projectNo, String productId, String lotNo, Instant inboundAt,
Instant outboundAt, boolean reserved, String rowVersion) {
this(spaceId, zoneCode, laneCode, depthOrder, laneCapacity, floorNo, aisleNo, groupCoding, rowCode,
role, status, outboundTaskActive, inventoryProjectConflict, trayNo, projectNo, productId,
lotNo, inboundAt, outboundAt, reserved, rowVersion, false);
}
public boolean hasTray() {
return trayNo != null && !trayNo.isBlank();
}
public boolean occupied() {
return status == SpaceStatus.OCCUPIED || hasTray() || physicalStockPresent;
}
public boolean availableEmpty() {
return status == SpaceStatus.EMPTY && !reserved && !hasTray() && !physicalStockPresent;
}
public boolean outboundSelectable() {
return status == SpaceStatus.OCCUPIED && !reserved && hasTray();
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.model;
public enum SpaceStatus {
EMPTY,
OCCUPIED,
DISABLED
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.algorithm.outbound;
import java.time.Instant;
import java.util.Optional;
public record FifoDecision(
boolean fifoApplied,
String fifoMode,
Integer fifoRank,
Instant oldestInboundTime,
Instant selectedInboundTime,
int fifoDeviationMinutes,
boolean fifoViolation,
String reason,
Optional<FifoWarning> warning
) {
public FifoDecision {
warning = warning == null ? Optional.empty() : warning;
}
public static FifoDecision off() {
return new FifoDecision(false, "OFF", null, null, null, 0, false,
"FIFO is disabled", Optional.empty());
}
}

View File

@@ -0,0 +1,4 @@
package com.ruoyi.algorithm.outbound;
public record FifoWarning(String code, String message) {
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.algorithm.outbound;
import java.util.List;
import java.util.Optional;
public record OutboundAssignment(
String requestId,
String trayNo,
String projectNo,
String productId,
String sourceSpaceId,
int sourceFloor,
boolean wholeTray,
List<String> blockerSpaceIds,
List<String> blockingTrayNos,
boolean laneReleasing,
double capacityScore,
double efficiencyScore,
double totalScore,
FifoDecision fifo,
Optional<OutboundWorkflow> workflow,
String reason
) {
public OutboundAssignment {
blockerSpaceIds = List.copyOf(blockerSpaceIds);
blockingTrayNos = List.copyOf(blockingTrayNos);
workflow = workflow == null ? Optional.empty() : workflow;
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.outbound;
public enum OutboundCapacityMode {
BALANCED,
WARNING,
CAPACITY_FIRST
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.algorithm.outbound;
import com.ruoyi.algorithm.model.SpaceState;
import java.util.List;
import java.util.Map;
import java.util.Set;
public record OutboundContext(
List<SpaceState> spaces,
Map<String, String> rawInboundTimes,
OutboundCapacityMode capacityMode,
Set<Integer> activeDeepWorkflowFloors
) {
public OutboundContext {
spaces = List.copyOf(spaces);
rawInboundTimes = Map.copyOf(rawInboundTimes);
capacityMode = capacityMode == null ? OutboundCapacityMode.BALANCED : capacityMode;
activeDeepWorkflowFloors = activeDeepWorkflowFloors == null ? Set.of() : Set.copyOf(activeDeepWorkflowFloors);
}
public OutboundContext(List<SpaceState> spaces, Map<String, String> rawInboundTimes,
OutboundCapacityMode capacityMode) {
this(spaces, rawInboundTimes, capacityMode, Set.of());
}
public String rawInboundTime(SpaceState space) {
if (rawInboundTimes.containsKey(space.spaceId())) {
return rawInboundTimes.get(space.spaceId());
}
return space.inboundAt() == null ? null : space.inboundAt().toString();
}
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.algorithm.outbound;
import java.util.Map;
public record OutboundError(OutboundErrorCode code, String message, Map<String, Object> details) {
public OutboundError {
details = Map.copyOf(details);
}
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.algorithm.outbound;
public enum OutboundErrorCode {
STOCK_NOT_FOUND,
AMBIGUOUS_TRAY,
OUTBOUND_SCOPE_REQUIRED,
FIFO_INVALID_MODE,
FIFO_INVALID_TOLERANCE,
FIFO_SCOPE_REQUIRED,
FIFO_DATA_MISSING,
FIFO_INVALID_TIME
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.algorithm.outbound;
import java.util.Optional;
public record OutboundPlan(
OutboundPlanStatus status,
Optional<OutboundAssignment> assignment,
Optional<OutboundWaiting> waiting,
Optional<OutboundError> error,
TaskCountEstimate taskCountEstimate
) {
public OutboundPlan {
assignment = assignment == null ? Optional.empty() : assignment;
waiting = waiting == null ? Optional.empty() : waiting;
error = error == null ? Optional.empty() : error;
}
public static OutboundPlan ready(OutboundAssignment assignment, TaskCountEstimate estimate) {
return new OutboundPlan(OutboundPlanStatus.READY, Optional.of(assignment), Optional.empty(),
Optional.empty(), estimate);
}
public static OutboundPlan waiting(OutboundWaiting waiting) {
return new OutboundPlan(OutboundPlanStatus.WAITING, Optional.empty(), Optional.of(waiting),
Optional.empty(), TaskCountEstimate.zero());
}
public static OutboundPlan error(OutboundError error) {
return new OutboundPlan(OutboundPlanStatus.ERROR, Optional.empty(), Optional.empty(),
Optional.of(error), TaskCountEstimate.zero());
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.outbound;
public enum OutboundPlanStatus {
READY,
WAITING,
ERROR
}

View File

@@ -0,0 +1,324 @@
package com.ruoyi.algorithm.outbound;
import com.ruoyi.algorithm.model.SpaceRole;
import com.ruoyi.algorithm.model.SpaceState;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
public final class OutboundPlanner {
public OutboundPlan plan(OutboundRequest request, OutboundContext context) {
String mode = normalizedMode(request.fifoMode());
if (!List.of("OFF", "STRICT", "FLEXIBLE").contains(mode)) {
return error(OutboundErrorCode.FIFO_INVALID_MODE, "Unsupported FIFO mode",
Map.of("fifoMode", String.valueOf(request.fifoMode())));
}
int tolerance = request.fifoToleranceMinutes() == null ? 0 : request.fifoToleranceMinutes();
if (tolerance < 0) {
return error(OutboundErrorCode.FIFO_INVALID_TOLERANCE,
"FIFO tolerance must be a non-negative integer", Map.of("fifoToleranceMinutes", tolerance));
}
if (blank(request.trayNo()) && blank(request.projectNo())) {
return error(OutboundErrorCode.OUTBOUND_SCOPE_REQUIRED,
"Outbound without a specified tray requires projectNo", Map.of("requestId", request.requestId()));
}
List<SpaceState> physicalMatches = context.spaces().stream()
.filter(SpaceState::occupied)
.filter(space -> matchesRequest(space, request))
.toList();
if (physicalMatches.isEmpty()) {
return error(OutboundErrorCode.STOCK_NOT_FOUND, "No matching stock was found",
Map.of("requestId", request.requestId()));
}
if (!blank(request.trayNo()) && physicalMatches.stream().noneMatch(SpaceState::outboundSelectable)) {
return unavailableSource(physicalMatches.get(0));
}
List<SpaceState> businessMatches = physicalMatches.stream()
.filter(SpaceState::outboundSelectable)
.toList();
if (businessMatches.isEmpty()) {
return unavailableSource(physicalMatches.get(0));
}
Selection selection = mode.equals("OFF")
? selectOff(businessMatches, request, context)
: selectFifo(businessMatches, request, context, mode, tolerance);
if (selection.error() != null) return OutboundPlan.error(selection.error());
SpaceState source = selection.source();
List<SpaceState> blockers = actualBlockers(source, context.spaces());
List<SpaceState> unavailableBlockers = blockers.stream()
.filter(blocker -> !blocker.outboundSelectable())
.toList();
if (!unavailableBlockers.isEmpty()) {
return OutboundPlan.waiting(new OutboundWaiting(OutboundWaitingCode.WAITING_CHANNEL,
blockers.size(), 0,
"A physical outer pallet is allocated, inspected, locked, or reserved; wait and recalculate",
true, Map.of("sourceSpaceId", source.spaceId(), "laneCode", source.laneCode(),
"blockingSpaceIds", blockers.stream().map(SpaceState::spaceId).toList(),
"blockingTrayNos", blockers.stream().map(SpaceState::trayNo).toList(),
"unavailableBlockingSpaceIds", unavailableBlockers.stream().map(SpaceState::spaceId).toList())));
}
List<SpaceState> buffers = availableBuffers(source.floorNo(), context.spaces());
if (!blockers.isEmpty() && context.activeDeepWorkflowFloors().contains(source.floorNo())) {
return OutboundPlan.waiting(new OutboundWaiting(
OutboundWaitingCode.WAITING_RELOCATION_BUFFER,
blockers.size(), buffers.size(),
"An active same-floor deep workflow owns the relocation buffer; wait and recalculate",
true, Map.of("floor", source.floorNo(), "sourceSpaceId", source.spaceId(),
"laneCode", source.laneCode())));
}
if (buffers.size() < blockers.size()) {
List<String> blockerIds = blockers.stream().map(SpaceState::spaceId).toList();
List<String> blockerTrays = blockers.stream().map(SpaceState::trayNo).toList();
Map<String, Object> details = new HashMap<>();
details.put("floor", source.floorNo());
details.put("required", blockers.size());
details.put("available", buffers.size());
details.put("sourceSpaceId", source.spaceId());
details.put("laneCode", source.laneCode());
details.put("blockingSpaceIds", blockerIds);
details.put("blockingTrayNos", blockerTrays);
return OutboundPlan.waiting(new OutboundWaiting(
OutboundWaitingCode.WAITING_RELOCATION_BUFFER,
blockers.size(), buffers.size(),
"Insufficient same-floor relocation buffer; wait and recalculate", true, details));
}
boolean laneReleasing = context.capacityMode() != OutboundCapacityMode.BALANCED
&& occupiedInLane(source, context.spaces()) == 1;
Optional<OutboundWorkflow> workflow = blockers.isEmpty()
? Optional.empty()
: Optional.of(buildWorkflow(request, source, blockers, buffers.subList(0, blockers.size()), context.spaces()));
OutboundAssignment assignment = new OutboundAssignment(
request.requestId(), source.trayNo(), source.projectNo(), source.productId(), source.spaceId(),
source.floorNo(), request.wholeTrayOrDefault(), blockers.stream().map(SpaceState::spaceId).toList(),
blockers.stream().map(SpaceState::trayNo).toList(), laneReleasing, 100.0,
efficiencyScore(source, request, context), 100.0, selection.fifo(), workflow,
"Outbound " + source.spaceId() + " with " + blockers.size() + " blocking tray(s)");
TaskCountEstimate estimate = new TaskCountEstimate(0, 1, blockers.size() * 2,
source.floorNo() == request.exitFloorOrDefault() ? 0 : 1);
return OutboundPlan.ready(assignment, estimate);
}
private Selection selectOff(List<SpaceState> matches, OutboundRequest request, OutboundContext context) {
Comparator<SpaceState> comparator = Comparator
.comparingInt((SpaceState space) -> actualBlockers(space, context.spaces()).size())
.thenComparingInt(space -> laneReleaseRank(space, context))
.thenComparingInt(SpaceState::floorNo)
.thenComparing(SpaceState::spaceId);
return new Selection(matches.stream().min(comparator).orElseThrow(), FifoDecision.off(), null);
}
private Selection selectFifo(List<SpaceState> requestMatches, OutboundRequest request,
OutboundContext context, String mode, int tolerance) {
SpaceState specified = blank(request.trayNo()) ? null : requestMatches.get(0);
String project = specified == null ? request.projectNo() : specified.projectNo();
String product = specified == null ? request.productId() : specified.productId();
if (blank(project)) {
return failed(OutboundErrorCode.FIFO_SCOPE_REQUIRED,
"FIFO scope requires a project or a tray that identifies one", Map.of("requestId", request.requestId()));
}
List<SpaceState> scope = context.spaces().stream()
.filter(SpaceState::occupied)
.filter(space -> project.equals(space.projectNo()))
.filter(space -> blank(product) || product.equals(space.productId()))
.toList();
List<String> missing = scope.stream()
.filter(space -> blank(context.rawInboundTime(space)))
.map(SpaceState::spaceId)
.sorted()
.toList();
if (!missing.isEmpty()) {
return failed(OutboundErrorCode.FIFO_DATA_MISSING,
"FIFO scope contains stock without inbound time", Map.of("spaceIds", missing));
}
List<TimedStock> timed = new ArrayList<>();
for (SpaceState space : scope) {
String raw = context.rawInboundTime(space);
Instant timestamp;
try {
timestamp = OffsetDateTime.parse(raw).toInstant();
} catch (DateTimeParseException first) {
try {
timestamp = Instant.parse(raw);
} catch (DateTimeParseException second) {
return failed(OutboundErrorCode.FIFO_INVALID_TIME, "Stock has an invalid inbound time",
Map.of("spaceId", space.spaceId(), "inboundTime", raw));
}
}
timed.add(new TimedStock(space, timestamp, actualBlockers(space, context.spaces()).size(),
laneReleaseRank(space, context), efficiencyScore(space, request, context)));
}
Instant oldest = timed.stream().map(TimedStock::timestamp).min(Instant::compareTo).orElseThrow();
Comparator<TimedStock> costComparator = Comparator.comparingInt(TimedStock::blockerCount)
.thenComparingInt(TimedStock::releaseRank)
.thenComparing(Comparator.comparingInt(TimedStock::efficiencyScore).reversed())
.thenComparing(TimedStock::timestamp)
.thenComparing(item -> item.space().spaceId());
TimedStock selected;
if (specified != null) {
selected = timed.stream().filter(item -> item.space().spaceId().equals(specified.spaceId()))
.findFirst().orElseThrow();
} else if (mode.equals("STRICT")) {
selected = timed.stream().filter(item -> item.timestamp().equals(oldest)).min(costComparator).orElseThrow();
} else {
Instant latest = oldest.plus(tolerance, ChronoUnit.MINUTES);
selected = timed.stream().filter(item -> !item.timestamp().isAfter(latest)).min(costComparator).orElseThrow();
}
int rank = 1 + (int) timed.stream().filter(item -> item.timestamp().isBefore(selected.timestamp())).count();
int deviation = Math.toIntExact(ChronoUnit.MINUTES.between(oldest, selected.timestamp()));
boolean overridden = specified != null;
boolean violation = overridden && selected.timestamp().isAfter(oldest);
Optional<FifoWarning> warning = overridden
? Optional.of(new FifoWarning("FIFO_OVERRIDDEN_BY_TRAY", violation
? "Specified tray is not the oldest stock; FIFO was explicitly overridden"
: "Specified tray is in the oldest FIFO cohort"))
: Optional.empty();
String reason = overridden
? "Specified tray retained; real FIFO rank reported"
: mode.equals("STRICT")
? "Global oldest stock selected"
: "Lowest handling cost selected inside the FIFO tolerance window";
return new Selection(selected.space(), new FifoDecision(true, mode, rank, oldest, selected.timestamp(),
deviation, violation, reason, warning), null);
}
private OutboundWorkflow buildWorkflow(OutboundRequest request, SpaceState source, List<SpaceState> blockers,
List<SpaceState> buffers, List<SpaceState> spaces) {
List<DraftStep> drafts = new ArrayList<>();
for (int index = 0; index < blockers.size(); index++) {
drafts.add(new DraftStep(id(request.requestId(), "MOVE-OUT", index), OutboundWorkflowPhase.MOVE_OUT,
blockers.get(index).spaceId(), buffers.get(index).spaceId()));
}
drafts.add(new DraftStep(id(request.requestId(), "OUTBOUND", 0), OutboundWorkflowPhase.OUTBOUND,
source.spaceId(), "DIRECT_OUT"));
List<SpaceState> ascending = new ArrayList<>(blockers);
ascending.sort(Comparator.comparingInt(SpaceState::groupCoding).thenComparing(SpaceState::spaceId));
List<SpaceState> compactTargets = spaces.stream()
.filter(space -> space.laneCode().equals(source.laneCode()))
.filter(space -> space.groupCoding() >= source.groupCoding())
.sorted(Comparator.comparingInt(SpaceState::groupCoding).thenComparing(SpaceState::spaceId))
.limit(blockers.size())
.toList();
for (int index = 0; index < ascending.size(); index++) {
SpaceState blocker = ascending.get(index);
int bufferIndex = blockers.indexOf(blocker);
String target = request.wholeTrayOrDefault() ? compactTargets.get(index).spaceId() : blocker.spaceId();
drafts.add(new DraftStep(id(request.requestId(), "RESTORE", index), OutboundWorkflowPhase.RESTORE,
buffers.get(bufferIndex).spaceId(), target));
}
List<OutboundWorkflowStep> steps = new ArrayList<>();
for (int index = 0; index < drafts.size(); index++) {
DraftStep draft = drafts.get(index);
steps.add(new OutboundWorkflowStep(draft.assignmentId(), draft.phase(), draft.sourceSpaceId(),
draft.targetSpaceId(), index + 1, drafts.size(), index == 0 ? null : drafts.get(index - 1).assignmentId()));
}
List<String> laneLocks = spaces.stream()
.filter(space -> space.laneCode().equals(source.laneCode()))
.sorted(Comparator.comparingInt(SpaceState::groupCoding).thenComparing(SpaceState::spaceId))
.map(SpaceState::spaceId)
.toList();
return new OutboundWorkflow(request.requestId(), source.spaceId(), source.laneCode(), source.floorNo(), steps,
laneLocks, buffers.stream().map(SpaceState::spaceId).toList(), "RELEASE_AFTER_FINAL_RESTORE");
}
private List<SpaceState> actualBlockers(SpaceState source, List<SpaceState> spaces) {
return spaces.stream()
.filter(space -> space.laneCode().equals(source.laneCode()))
.filter(space -> space.groupCoding() > source.groupCoding())
.filter(SpaceState::occupied)
.sorted(Comparator.comparingInt(SpaceState::groupCoding).reversed()
.thenComparing(SpaceState::spaceId))
.toList();
}
private List<SpaceState> availableBuffers(int floor, List<SpaceState> spaces) {
return spaces.stream()
.filter(space -> space.floorNo() == floor)
.filter(space -> space.role() == SpaceRole.RELOCATION_BUFFER)
.filter(SpaceState::availableEmpty)
.sorted(Comparator.comparing(SpaceState::spaceId))
.toList();
}
private boolean matchesRequest(SpaceState space, OutboundRequest request) {
if (!blank(request.trayNo())) return request.trayNo().equals(space.trayNo());
return request.projectNo().equals(space.projectNo())
&& (blank(request.productId()) || request.productId().equals(space.productId()));
}
private int laneReleaseRank(SpaceState space, OutboundContext context) {
return context.capacityMode() != OutboundCapacityMode.BALANCED
&& occupiedInLane(space, context.spaces()) == 1 ? 0 : 1;
}
private int occupiedInLane(SpaceState source, List<SpaceState> spaces) {
return (int) spaces.stream()
.filter(space -> space.laneCode().equals(source.laneCode()))
.filter(SpaceState::occupied)
.count();
}
private int efficiencyScore(SpaceState space, OutboundRequest request, OutboundContext context) {
return 100 - Math.abs(space.floorNo() - request.exitFloorOrDefault()) * 12
- actualBlockers(space, context.spaces()).size() * 10;
}
private OutboundPlan error(OutboundErrorCode code, String message, Map<String, Object> details) {
return OutboundPlan.error(new OutboundError(code, message, details));
}
private OutboundPlan unavailableSource(SpaceState source) {
Map<String, Object> details = new HashMap<>();
details.put("sourceSpaceId", source.spaceId());
details.put("laneCode", source.laneCode());
if (!blank(source.trayNo())) details.put("trayNo", source.trayNo());
return OutboundPlan.waiting(new OutboundWaiting(OutboundWaitingCode.WAITING_CHANNEL, 0, 0,
"Requested stock is allocated, inspected, locked, or reserved; wait and recalculate", true,
details));
}
private Selection failed(OutboundErrorCode code, String message, Map<String, Object> details) {
return new Selection(null, null, new OutboundError(code, message, details));
}
private String normalizedMode(String value) {
return blank(value) ? "OFF" : value.trim().toUpperCase(Locale.ROOT);
}
private String id(String requestId, String phase, int index) {
return requestId + "-" + phase + "-" + String.format(Locale.ROOT, "%02d", index + 1);
}
private boolean blank(String value) {
return value == null || value.isBlank();
}
private record Selection(SpaceState source, FifoDecision fifo, OutboundError error) {
}
private record TimedStock(SpaceState space, Instant timestamp, int blockerCount, int releaseRank,
int efficiencyScore) {
}
private record DraftStep(String assignmentId, OutboundWorkflowPhase phase, String sourceSpaceId,
String targetSpaceId) {
}
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.algorithm.outbound;
public record OutboundRequest(
String requestId,
String trayNo,
String projectNo,
String productId,
String fifoMode,
Integer fifoToleranceMinutes,
Integer exitFloor,
Boolean wholeTray
) {
public boolean wholeTrayOrDefault() {
return wholeTray == null || wholeTray;
}
public int exitFloorOrDefault() {
return exitFloor == null ? 1 : exitFloor;
}
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.algorithm.outbound;
import java.util.Map;
public record OutboundWaiting(
OutboundWaitingCode code,
int required,
int available,
String reason,
boolean retryable,
Map<String, Object> details
) {
public OutboundWaiting {
details = Map.copyOf(details);
}
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.algorithm.outbound;
public enum OutboundWaitingCode {
WAITING_RELOCATION_BUFFER,
WAITING_CHANNEL
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.algorithm.outbound;
import java.util.List;
public record OutboundWorkflow(
String workflowId,
String sourceSpaceId,
String laneCode,
int floorNo,
List<OutboundWorkflowStep> steps,
List<String> lockedLaneSpaceIds,
List<String> lockedBufferSpaceIds,
String lockReleaseRule
) {
public OutboundWorkflow {
steps = List.copyOf(steps);
lockedLaneSpaceIds = List.copyOf(lockedLaneSpaceIds);
lockedBufferSpaceIds = List.copyOf(lockedBufferSpaceIds);
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.outbound;
public enum OutboundWorkflowPhase {
MOVE_OUT,
OUTBOUND,
RESTORE
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.algorithm.outbound;
public record OutboundWorkflowStep(
String assignmentId,
OutboundWorkflowPhase phase,
String sourceSpaceId,
String targetSpaceId,
int workflowStep,
int workflowTotalSteps,
String dependsOnAssignmentId
) {
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.outbound;
public record TaskCountEstimate(int inbound, int outbound, int relocation, int floorChange) {
public static TaskCountEstimate zero() {
return new TaskCountEstimate(0, 0, 0, 0);
}
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.algorithm.policy;
public record SchedulingPolicy(
double capacityWeight,
double efficiencyWeight,
int warningOccupied,
int capacityFirstOccupied,
int maxOccupied,
int relocationBuffersPerFloor,
int outboundCachesPerFloor,
int maxTidyCandidates
) {
public static SchedulingPolicy ft546Defaults() {
return new SchedulingPolicy(0.55, 0.45, 564, 597, 615, 4, 2, 10);
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.repository;
import java.time.Instant;
public record DecisionLogEntry(String decisionId, String warehouseId, String decisionType,
VersionToken inputVersion, String resultCode, String reason, Instant createdAt) {
}

View File

@@ -0,0 +1,5 @@
package com.ruoyi.algorithm.repository;
public record DecisionLogRecord(String decisionId, String warehouseId, String decisionType,
VersionToken inputVersion, String resultCode, String reason) {
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.algorithm.repository;
public record DeepWorkflowMutationResult(boolean changed, String reasonCode) {
public static DeepWorkflowMutationResult succeeded() { return new DeepWorkflowMutationResult(true, null); }
public static DeepWorkflowMutationResult unchanged() { return new DeepWorkflowMutationResult(false, null); }
public static DeepWorkflowMutationResult writesDisabled() {
return new DeepWorkflowMutationResult(false, "WRITES_DISABLED");
}
}

View File

@@ -0,0 +1,61 @@
package com.ruoyi.algorithm.repository;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Objects;
final class H2TopologyReadModel {
private final JdbcTemplate jdbc;
H2TopologyReadModel(JdbcTemplate jdbc) {
this.jdbc = Objects.requireNonNull(jdbc, "jdbc");
}
boolean changed(String warehouseId) {
String expectedMismatch = "WITH LANE_TOPOLOGY AS ("
+ "SELECT WAREHOUSE_ID,GROUP_CODING_NO AS LANE_CODE,COUNT(*) AS LANE_DEPTH,"
+ "MAX(THE_COLUMN) AS COLUMN_NO,MIN(THE_PLATOON) AS ROW_NO,MIN(THE_FLOOR) AS FLOOR_NO,"
+ "MIN(CARGO_SPACE_ID) AS STABLE_ID FROM BAS_CARGO_SPACE WHERE WAREHOUSE_ID=? "
+ "GROUP BY WAREHOUSE_ID,GROUP_CODING_NO), RANKED_LANES AS ("
+ "SELECT LANE_TOPOLOGY.*,CASE WHEN LANE_DEPTH=1 THEN ROW_NUMBER() OVER ("
+ "PARTITION BY WAREHOUSE_ID ORDER BY CASE WHEN LANE_DEPTH=1 THEN 0 ELSE 1 END,"
+ "COLUMN_NO DESC,ROW_NO ASC,FLOOR_NO ASC,STABLE_ID ASC) END AS SINGLE_DEPTH_RANK "
+ "FROM LANE_TOPOLOGY), EXPECTED AS (SELECT C.CARGO_SPACE_ID AS SPACE_ID,C.WAREHOUSE_ID,"
+ "C.GROUP_CODING_NO AS LANE_CODE,R.LANE_DEPTH,CASE WHEN R.LANE_DEPTH=1 "
+ "AND R.SINGLE_DEPTH_RANK<=80 THEN 'CACHE' WHEN R.LANE_DEPTH=1 AND R.SINGLE_DEPTH_RANK<=133 "
+ "THEN 'RESERVE' WHEN R.LANE_DEPTH=5 THEN 'MAIN' ELSE 'FLEX' END AS BASE_ZONE "
+ "FROM BAS_CARGO_SPACE C JOIN RANKED_LANES R ON R.WAREHOUSE_ID=C.WAREHOUSE_ID "
+ "AND R.LANE_CODE=C.GROUP_CODING_NO WHERE C.WAREHOUSE_ID=?) "
+ "SELECT COUNT(*) FROM EXPECTED E LEFT JOIN ALG_SPACE_TOPOLOGY T ON T.SPACE_ID=E.SPACE_ID "
+ "WHERE T.SPACE_ID IS NULL OR T.WAREHOUSE_ID<>E.WAREHOUSE_ID OR T.LANE_CODE<>E.LANE_CODE "
+ "OR T.LANE_DEPTH<>E.LANE_DEPTH OR T.BASE_ZONE<>E.BASE_ZONE";
Integer mismatched = jdbc.queryForObject(expectedMismatch, Integer.class, warehouseId, warehouseId);
if (mismatched != null && mismatched > 0) {
return true;
}
Integer stale = jdbc.queryForObject("SELECT COUNT(*) FROM ALG_SPACE_TOPOLOGY T LEFT JOIN BAS_CARGO_SPACE C "
+ "ON C.CARGO_SPACE_ID=T.SPACE_ID AND C.WAREHOUSE_ID=T.WAREHOUSE_ID "
+ "WHERE T.WAREHOUSE_ID=? AND C.CARGO_SPACE_ID IS NULL",
Integer.class, warehouseId);
return stale != null && stale > 0;
}
void rebuild(String warehouseId) {
jdbc.update("DELETE FROM ALG_SPACE_TOPOLOGY WHERE WAREHOUSE_ID=?", warehouseId);
jdbc.update("INSERT INTO ALG_SPACE_TOPOLOGY (WAREHOUSE_ID,SPACE_ID,LANE_CODE,LANE_DEPTH,BASE_ZONE) "
+ "WITH LANE_TOPOLOGY AS (SELECT WAREHOUSE_ID,GROUP_CODING_NO AS LANE_CODE,"
+ "COUNT(*) AS LANE_DEPTH,MAX(THE_COLUMN) AS COLUMN_NO,MIN(THE_PLATOON) AS ROW_NO,"
+ "MIN(THE_FLOOR) AS FLOOR_NO,MIN(CARGO_SPACE_ID) AS STABLE_ID FROM BAS_CARGO_SPACE "
+ "WHERE WAREHOUSE_ID=? GROUP BY WAREHOUSE_ID,GROUP_CODING_NO), RANKED_LANES AS ("
+ "SELECT LANE_TOPOLOGY.*,CASE WHEN LANE_DEPTH=1 THEN ROW_NUMBER() OVER ("
+ "PARTITION BY WAREHOUSE_ID ORDER BY CASE WHEN LANE_DEPTH=1 THEN 0 ELSE 1 END,"
+ "COLUMN_NO DESC,ROW_NO ASC,FLOOR_NO ASC,STABLE_ID ASC) END AS SINGLE_DEPTH_RANK "
+ "FROM LANE_TOPOLOGY) SELECT C.WAREHOUSE_ID,C.CARGO_SPACE_ID,C.GROUP_CODING_NO,"
+ "R.LANE_DEPTH,CASE WHEN R.LANE_DEPTH=1 AND R.SINGLE_DEPTH_RANK<=80 THEN 'CACHE' "
+ "WHEN R.LANE_DEPTH=1 AND R.SINGLE_DEPTH_RANK<=133 THEN 'RESERVE' "
+ "WHEN R.LANE_DEPTH=5 THEN 'MAIN' ELSE 'FLEX' END FROM BAS_CARGO_SPACE C "
+ "JOIN RANKED_LANES R ON R.WAREHOUSE_ID=C.WAREHOUSE_ID "
+ "AND R.LANE_CODE=C.GROUP_CODING_NO WHERE C.WAREHOUSE_ID=?",
warehouseId, warehouseId);
}
}

View File

@@ -0,0 +1,5 @@
package com.ruoyi.algorithm.repository;
public record InboundReservation(String reservationId, String warehouseId, String laneCode,
String spaceId, String projectNo, VersionToken expectedVersion) {
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.algorithm.repository;
import java.math.BigDecimal;
import java.time.Instant;
public record InventoryItem(String stockId, String spaceId, String trayNo, String projectNo, String productNo,
String lotNo, String inboundTimeRaw, Instant inboundAt,
BigDecimal quantity, BigDecimal outboundQuantity) {
}

View File

@@ -0,0 +1,845 @@
package com.ruoyi.algorithm.repository;
import com.ruoyi.algorithm.inbound.InboundPolicy;
import com.ruoyi.algorithm.model.SpaceRole;
import com.ruoyi.algorithm.model.SpaceState;
import com.ruoyi.algorithm.model.SpaceStatus;
import com.ruoyi.algorithm.policy.SchedulingPolicy;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.sql.Timestamp;
import java.time.Instant;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
public final class JdbcWarehouseRepository implements WarehouseRepository {
public static final int MAX_PAGE_SIZE = 200;
private final RepositoryDialect dialect;
private final JdbcTemplate jdbc;
private final NamedParameterJdbcTemplate named;
private final TransactionTemplate transactions;
private final H2TopologyReadModel h2Topology;
private final boolean writesEnabled;
public JdbcWarehouseRepository(DataSource dataSource, RepositoryDialect dialect) {
this(dataSource, dialect, true);
}
public JdbcWarehouseRepository(DataSource dataSource, RepositoryDialect dialect, boolean writesEnabled) {
this(dataSource, dialect, writesEnabled, new DataSourceTransactionManager(dataSource));
}
public JdbcWarehouseRepository(DataSource dataSource, RepositoryDialect dialect, boolean writesEnabled,
PlatformTransactionManager transactionManager) {
this.dialect = Objects.requireNonNull(dialect, "dialect");
this.jdbc = new JdbcTemplate(dataSource);
this.named = new NamedParameterJdbcTemplate(dataSource);
this.transactions = new TransactionTemplate(Objects.requireNonNull(transactionManager, "transactionManager"));
this.h2Topology = new H2TopologyReadModel(jdbc);
this.writesEnabled = writesEnabled;
}
@Override
public void synchronizeReadModel(String warehouseId) {
if (!writesEnabled) throw new IllegalStateException("Database writes are disabled");
if (blankToNull(warehouseId) == null) throw new IllegalArgumentException("warehouseId is required");
if (!dialect.h2Database()) {
jdbc.update("EXEC alg.usp_sync_inventory_snapshot @warehouse_id=?", warehouseId);
jdbc.update("EXEC alg.usp_rebuild_lane_state @warehouse_id=?", warehouseId);
return;
}
transactions.executeWithoutResult(status -> synchronizeH2ReadModel(warehouseId));
}
@Override
public void refreshChangedLanes(String warehouseId) {
if (!writesEnabled) throw new IllegalStateException("Database writes are disabled");
if (blankToNull(warehouseId) == null) throw new IllegalArgumentException("warehouseId is required");
if (!dialect.h2Database()) {
jdbc.update("EXEC alg.usp_refresh_dirty_lanes @warehouse_id=?", warehouseId);
return;
}
transactions.executeWithoutResult(status -> refreshChangedLanesH2(warehouseId));
}
private void refreshChangedLanesH2(String warehouseId) {
LinkedHashSet<String> laneCodes = new LinkedHashSet<>(jdbc.queryForList(
"SELECT GROUP_CODING_NO FROM BAS_CARGO_SPACE WHERE WAREHOUSE_ID=? "
+ "UNION SELECT LANE_CODE FROM ALG_LANE_STATE WHERE WAREHOUSE_ID=? ORDER BY 1",
String.class, warehouseId, warehouseId));
if (h2Topology.changed(warehouseId)) {
h2Topology.rebuild(warehouseId);
laneCodes.addAll(jdbc.queryForList("SELECT DISTINCT GROUP_CODING_NO FROM BAS_CARGO_SPACE "
+ "WHERE WAREHOUSE_ID=? ORDER BY GROUP_CODING_NO", String.class, warehouseId));
jdbc.update("DELETE FROM ALG_INVENTORY_ITEM_SNAPSHOT WHERE WAREHOUSE_ID=? AND SPACE_ID NOT IN "
+ "(SELECT CARGO_SPACE_ID FROM BAS_CARGO_SPACE WHERE WAREHOUSE_ID=?)",
warehouseId, warehouseId);
jdbc.update("DELETE FROM ALG_SPACE_INVENTORY_SUMMARY WHERE WAREHOUSE_ID=? AND SPACE_ID NOT IN "
+ "(SELECT CARGO_SPACE_ID FROM BAS_CARGO_SPACE WHERE WAREHOUSE_ID=?)",
warehouseId, warehouseId);
}
List<String> dirtyLanes = laneCodes.stream().filter(laneCode -> {
List<String> stored = jdbc.queryForList("SELECT SEMANTIC_FINGERPRINT FROM ALG_LANE_STATE "
+ "WHERE WAREHOUSE_ID=? AND LANE_CODE=?",
String.class, warehouseId, laneCode);
return stored.isEmpty() || !Objects.equals(stored.get(0),
h2SemanticFingerprint(warehouseId, laneCode, InventoryFingerprintSource.LIVE));
}).toList();
dirtyLanes.forEach(laneCode -> refreshH2InventoryLane(warehouseId, laneCode));
dirtyLanes.forEach(laneCode -> refreshH2LaneState(warehouseId, laneCode));
}
private void refreshH2InventoryLane(String warehouseId, String laneCode) {
jdbc.update("DELETE FROM ALG_SPACE_INVENTORY_SUMMARY WHERE WAREHOUSE_ID=? AND SPACE_ID IN "
+ "(SELECT CARGO_SPACE_ID FROM BAS_CARGO_SPACE WHERE WAREHOUSE_ID=? AND GROUP_CODING_NO=?)",
warehouseId, warehouseId, laneCode);
jdbc.update("DELETE FROM ALG_INVENTORY_ITEM_SNAPSHOT WHERE WAREHOUSE_ID=? AND SPACE_ID IN "
+ "(SELECT CARGO_SPACE_ID FROM BAS_CARGO_SPACE WHERE WAREHOUSE_ID=? AND GROUP_CODING_NO=?)",
warehouseId, warehouseId, laneCode);
jdbc.update("INSERT INTO ALG_INVENTORY_ITEM_SNAPSHOT "
+ "(WAREHOUSE_ID,STOCK_ID,SPACE_ID,TRAY_CODE,PROJECT_NO,PRODUCT_NO,LOT_NO,"
+ "INBOUND_TIME_RAW,INBOUND_AT,QUANTITY,OUT_QUANTITY) "
+ "SELECT C.WAREHOUSE_ID,S.STOCK_ID,S.CARGO_SPACE_ID,S.TRAY_CODE,S.SPARE4,S.PRODUCT_ID,"
+ "S.LOT_NO,S.IN_DATE,ALG_TRY_TIMESTAMP(S.IN_DATE),S.QUANTITY,S.OUT_QUANTITY "
+ "FROM BAS_STOCK S JOIN BAS_CARGO_SPACE C ON C.CARGO_SPACE_ID=S.CARGO_SPACE_ID "
+ "WHERE C.WAREHOUSE_ID=? AND C.GROUP_CODING_NO=?",
warehouseId, laneCode);
jdbc.update("INSERT INTO ALG_SPACE_INVENTORY_SUMMARY "
+ "(WAREHOUSE_ID,SPACE_ID,STOCK_DETAIL_COUNT,STOCK_TRAY_COUNT,STOCK_PROJECT_COUNT,STOCK_PRODUCT_COUNT) "
+ "SELECT I.WAREHOUSE_ID,I.SPACE_ID,COUNT(*),COUNT(DISTINCT NULLIF(TRIM(I.TRAY_CODE),'')),"
+ "COUNT(DISTINCT NULLIF(TRIM(I.PROJECT_NO),'')),COUNT(DISTINCT NULLIF(TRIM(I.PRODUCT_NO),'')) "
+ "FROM ALG_INVENTORY_ITEM_SNAPSHOT I JOIN BAS_CARGO_SPACE C ON C.CARGO_SPACE_ID=I.SPACE_ID "
+ "AND C.WAREHOUSE_ID=I.WAREHOUSE_ID WHERE I.WAREHOUSE_ID=? AND C.GROUP_CODING_NO=? "
+ "GROUP BY I.WAREHOUSE_ID,I.SPACE_ID",
warehouseId, laneCode);
}
private void refreshH2LaneState(String warehouseId, String laneCode) {
List<Map<String, Object>> source = jdbc.queryForList("SELECT WAREHOUSE_ID,LANE_CODE,COUNT(*) SPACE_COUNT,"
+ "SUM(CASE WHEN SPACE_STATUS='2' OR STOCK_DETAIL_COUNT>0 THEN 1 ELSE 0 END) OCCUPIED_COUNT,"
+ "SUM(CASE WHEN INBOUND_AVAILABLE=TRUE THEN 1 ELSE 0 END) AVAILABLE_COUNT,"
+ "MAX(CASE WHEN PENDING_OUTBOUND=TRUE THEN 1 ELSE 0 END) PENDING_OUTBOUND,"
+ "MAX(CASE WHEN HAS_GAP=TRUE THEN 1 ELSE 0 END) HAS_GAP "
+ "FROM ALG_V_SPACE_STATE WHERE WAREHOUSE_ID=? AND LANE_CODE=? "
+ "GROUP BY WAREHOUSE_ID,LANE_CODE",
warehouseId, laneCode);
if (source.isEmpty()) {
jdbc.update("DELETE FROM ALG_LANE_STATE WHERE WAREHOUSE_ID=? AND LANE_CODE=?",
warehouseId, laneCode);
return;
}
Map<String, Object> lane = source.get(0);
int spaces = number(lane, "SPACE_COUNT");
int occupied = number(lane, "OCCUPIED_COUNT");
int available = number(lane, "AVAILABLE_COUNT");
boolean pending = number(lane, "PENDING_OUTBOUND") != 0;
boolean gap = number(lane, "HAS_GAP") != 0;
String fingerprint = h2SemanticFingerprint(warehouseId, laneCode,
InventoryFingerprintSource.SNAPSHOT);
List<Map<String, Object>> existing = jdbc.queryForList(
"SELECT SPACE_COUNT,OCCUPIED_COUNT,AVAILABLE_COUNT,PENDING_OUTBOUND,HAS_GAP,"
+ "SEMANTIC_FINGERPRINT FROM ALG_LANE_STATE WHERE WAREHOUSE_ID=? AND LANE_CODE=?",
warehouseId, laneCode);
if (existing.isEmpty()) {
jdbc.update("INSERT INTO ALG_LANE_STATE (WAREHOUSE_ID,LANE_CODE,SPACE_COUNT,OCCUPIED_COUNT,"
+ "AVAILABLE_COUNT,PENDING_OUTBOUND,HAS_GAP,SEMANTIC_FINGERPRINT,STATE_VERSION) "
+ "VALUES (?,?,?,?,?,?,?,?,1)",
warehouseId, laneCode, spaces, occupied, available, pending, gap, fingerprint);
return;
}
Map<String, Object> old = existing.get(0);
boolean changed = number(old, "SPACE_COUNT") != spaces || number(old, "OCCUPIED_COUNT") != occupied
|| number(old, "AVAILABLE_COUNT") != available
|| booleanValue(old.get("PENDING_OUTBOUND")) != pending
|| booleanValue(old.get("HAS_GAP")) != gap
|| !Objects.equals(old.get("SEMANTIC_FINGERPRINT"), fingerprint);
if (changed) {
jdbc.update("UPDATE ALG_LANE_STATE SET SPACE_COUNT=?,OCCUPIED_COUNT=?,AVAILABLE_COUNT=?,"
+ "PENDING_OUTBOUND=?,HAS_GAP=?,SEMANTIC_FINGERPRINT=?,STATE_VERSION=STATE_VERSION+1,"
+ "REBUILT_AT=CURRENT_TIMESTAMP WHERE WAREHOUSE_ID=? AND LANE_CODE=?",
spaces, occupied, available, pending, gap, fingerprint, warehouseId, laneCode);
}
}
private void synchronizeH2ReadModel(String warehouseId) {
jdbc.update("DELETE FROM ALG_SPACE_INVENTORY_SUMMARY WHERE WAREHOUSE_ID = ?", warehouseId);
jdbc.update("DELETE FROM ALG_INVENTORY_ITEM_SNAPSHOT WHERE WAREHOUSE_ID = ?", warehouseId);
jdbc.update("INSERT INTO ALG_INVENTORY_ITEM_SNAPSHOT "
+ "(WAREHOUSE_ID,STOCK_ID,SPACE_ID,TRAY_CODE,PROJECT_NO,PRODUCT_NO,LOT_NO,"
+ "INBOUND_TIME_RAW,INBOUND_AT,QUANTITY,OUT_QUANTITY) "
+ "SELECT C.WAREHOUSE_ID,S.STOCK_ID,S.CARGO_SPACE_ID,S.TRAY_CODE,S.SPARE4,S.PRODUCT_ID,"
+ "S.LOT_NO,S.IN_DATE,ALG_TRY_TIMESTAMP(S.IN_DATE),S.QUANTITY,S.OUT_QUANTITY "
+ "FROM BAS_STOCK S JOIN BAS_CARGO_SPACE C ON C.CARGO_SPACE_ID=S.CARGO_SPACE_ID "
+ "WHERE C.WAREHOUSE_ID = ?", warehouseId);
jdbc.update("INSERT INTO ALG_SPACE_INVENTORY_SUMMARY "
+ "(WAREHOUSE_ID,SPACE_ID,STOCK_DETAIL_COUNT,STOCK_TRAY_COUNT,STOCK_PROJECT_COUNT,STOCK_PRODUCT_COUNT) "
+ "SELECT WAREHOUSE_ID,SPACE_ID,COUNT(*),COUNT(DISTINCT NULLIF(TRIM(TRAY_CODE),'')),"
+ "COUNT(DISTINCT NULLIF(TRIM(PROJECT_NO),'')),COUNT(DISTINCT NULLIF(TRIM(PRODUCT_NO),'')) "
+ "FROM ALG_INVENTORY_ITEM_SNAPSHOT WHERE WAREHOUSE_ID = ? GROUP BY WAREHOUSE_ID,SPACE_ID",
warehouseId);
List<Map<String, Object>> source = jdbc.queryForList("SELECT WAREHOUSE_ID,LANE_CODE,COUNT(*) SPACE_COUNT,"
+ "SUM(CASE WHEN SPACE_STATUS='2' OR STOCK_DETAIL_COUNT>0 THEN 1 ELSE 0 END) OCCUPIED_COUNT,"
+ "SUM(CASE WHEN INBOUND_AVAILABLE=TRUE THEN 1 ELSE 0 END) AVAILABLE_COUNT,"
+ "MAX(CASE WHEN PENDING_OUTBOUND=TRUE THEN 1 ELSE 0 END) PENDING_OUTBOUND,"
+ "MAX(CASE WHEN HAS_GAP=TRUE THEN 1 ELSE 0 END) HAS_GAP "
+ "FROM ALG_V_SPACE_STATE WHERE WAREHOUSE_ID=? GROUP BY WAREHOUSE_ID,LANE_CODE ORDER BY LANE_CODE",
warehouseId);
Set<String> currentLanes = new LinkedHashSet<>();
for (Map<String, Object> lane : source) {
String laneCode = lane.get("LANE_CODE").toString();
currentLanes.add(laneCode);
String fingerprint = h2SemanticFingerprint(warehouseId, laneCode,
InventoryFingerprintSource.SNAPSHOT);
List<Map<String, Object>> existing = jdbc.queryForList("SELECT SPACE_COUNT,OCCUPIED_COUNT,AVAILABLE_COUNT,"
+ "PENDING_OUTBOUND,HAS_GAP,SEMANTIC_FINGERPRINT,STATE_VERSION FROM ALG_LANE_STATE "
+ "WHERE WAREHOUSE_ID=? AND LANE_CODE=?", warehouseId, laneCode);
int spaces = number(lane, "SPACE_COUNT");
int occupied = number(lane, "OCCUPIED_COUNT");
int available = number(lane, "AVAILABLE_COUNT");
boolean pending = number(lane, "PENDING_OUTBOUND") != 0;
boolean gap = number(lane, "HAS_GAP") != 0;
if (existing.isEmpty()) {
jdbc.update("INSERT INTO ALG_LANE_STATE (WAREHOUSE_ID,LANE_CODE,SPACE_COUNT,OCCUPIED_COUNT,"
+ "AVAILABLE_COUNT,PENDING_OUTBOUND,HAS_GAP,SEMANTIC_FINGERPRINT,STATE_VERSION) "
+ "VALUES (?,?,?,?,?,?,?,?,1)", warehouseId, laneCode, spaces, occupied, available,
pending, gap, fingerprint);
continue;
}
Map<String, Object> old = existing.get(0);
boolean changed = number(old, "SPACE_COUNT") != spaces || number(old, "OCCUPIED_COUNT") != occupied
|| number(old, "AVAILABLE_COUNT") != available
|| booleanValue(old.get("PENDING_OUTBOUND")) != pending
|| booleanValue(old.get("HAS_GAP")) != gap
|| !fingerprint.equals(Objects.toString(old.get("SEMANTIC_FINGERPRINT"), ""));
if (changed) {
jdbc.update("UPDATE ALG_LANE_STATE SET SPACE_COUNT=?,OCCUPIED_COUNT=?,AVAILABLE_COUNT=?,"
+ "PENDING_OUTBOUND=?,HAS_GAP=?,SEMANTIC_FINGERPRINT=?,STATE_VERSION=STATE_VERSION+1,"
+ "REBUILT_AT=CURRENT_TIMESTAMP WHERE WAREHOUSE_ID=? AND LANE_CODE=?",
spaces, occupied, available, pending, gap, fingerprint, warehouseId, laneCode);
}
}
jdbc.queryForList("SELECT LANE_CODE FROM ALG_LANE_STATE WHERE WAREHOUSE_ID=?", String.class, warehouseId)
.stream().filter(lane -> !currentLanes.contains(lane))
.forEach(lane -> jdbc.update("DELETE FROM ALG_LANE_STATE WHERE WAREHOUSE_ID=? AND LANE_CODE=?",
warehouseId, lane));
}
private String h2SemanticFingerprint(String warehouseId, String laneCode,
InventoryFingerprintSource inventorySource) {
StringBuilder payload = new StringBuilder();
appendRows(payload, "SELECT C.CARGO_SPACE_ID,C.STATUS,C.LOCKED,C.TRAY_CODE,C.GROUP_CODING,"
+ "C.THE_PLATOON,C.THE_COLUMN,C.THE_FLOOR,T.LANE_DEPTH,T.BASE_ZONE,COALESCE(R.ROLE,'STORAGE') "
+ "FROM BAS_CARGO_SPACE C LEFT JOIN ALG_SPACE_TOPOLOGY T ON T.SPACE_ID=C.CARGO_SPACE_ID "
+ "LEFT JOIN ALG_SPACE_ROLE R ON R.SPACE_ID=C.CARGO_SPACE_ID "
+ "WHERE C.WAREHOUSE_ID=? AND C.GROUP_CODING_NO=? "
+ "ORDER BY CASE WHEN C.GROUP_CODING REGEXP '^[0-9]+$' THEN CAST(C.GROUP_CODING AS INT) END,C.CARGO_SPACE_ID",
warehouseId, laneCode);
String inventorySql = inventorySource == InventoryFingerprintSource.SNAPSHOT
? "SELECT S.STOCK_ID,S.SPACE_ID,S.TRAY_CODE,S.PROJECT_NO,S.PRODUCT_NO,S.LOT_NO,"
+ "S.INBOUND_TIME_RAW,CAST(S.QUANTITY AS DECIMAL(38,6)),CAST(S.OUT_QUANTITY AS DECIMAL(38,6)) "
+ "FROM ALG_INVENTORY_ITEM_SNAPSHOT S JOIN BAS_CARGO_SPACE C "
+ "ON C.CARGO_SPACE_ID=S.SPACE_ID AND C.WAREHOUSE_ID=S.WAREHOUSE_ID "
+ "WHERE C.WAREHOUSE_ID=? AND C.GROUP_CODING_NO=? ORDER BY S.SPACE_ID,S.STOCK_ID"
: "SELECT S.STOCK_ID,S.CARGO_SPACE_ID,S.TRAY_CODE,S.SPARE4,S.PRODUCT_ID,S.LOT_NO,"
+ "S.IN_DATE,CAST(S.QUANTITY AS DECIMAL(38,6)),CAST(S.OUT_QUANTITY AS DECIMAL(38,6)) "
+ "FROM BAS_STOCK S JOIN BAS_CARGO_SPACE C ON C.CARGO_SPACE_ID=S.CARGO_SPACE_ID "
+ "WHERE C.WAREHOUSE_ID=? AND C.GROUP_CODING_NO=? ORDER BY S.CARGO_SPACE_ID,S.STOCK_ID";
appendRows(payload, inventorySql, warehouseId, laneCode);
appendRows(payload, "SELECT JOB_ID,TASK_NO,TRAY_CODE,SOURCE_SPACE_ID,TARGET_CARGO_SPACE_ID,STATUS,IN_OR_OUT,CREATE_TIME_RAW "
+ "FROM ALG_V_ACTIVE_JOB WHERE WAREHOUSE_ID=? AND SOURCE_LANE_CODE=? ORDER BY JOB_ID",
warehouseId, laneCode);
appendRows(payload, "SELECT PROJECT_NO,IS_ISOLATED,IS_COMMON,ALLOW_MIXED FROM ALG_PROJECT_POLICY ORDER BY PROJECT_NO");
appendRows(payload, "SELECT WAREHOUSE_ID,TARGET_FILL_RATE,FIFO_TOLERANCE_HOURS FROM ALG_WAREHOUSE_POLICY WHERE WAREHOUSE_ID=?",
warehouseId);
appendRows(payload, "SELECT RESERVATION_ID,SPACE_ID,PROJECT_NO,STATUS FROM ALG_RESERVATION "
+ "WHERE WAREHOUSE_ID=? AND LANE_CODE=? AND STATUS='ACTIVE' ORDER BY SPACE_ID,RESERVATION_ID",
warehouseId, laneCode);
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(payload.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable", exception);
}
}
private void appendRows(StringBuilder payload, String sql, Object... arguments) {
payload.append('|');
jdbc.query(sql, rs -> {
int columns = rs.getMetaData().getColumnCount();
payload.append('[');
for (int index = 1; index <= columns; index++) {
String value = rs.getString(index);
payload.append(value == null ? -1 : value.length()).append(':');
if (value != null) payload.append(value);
payload.append(';');
}
payload.append(']');
}, arguments);
}
private int number(Map<String, Object> row, String column) {
Object value = row.get(column);
return value == null ? 0 : ((Number) value).intValue();
}
private boolean booleanValue(Object value) {
return value instanceof Boolean bool ? bool : value instanceof Number number && number.intValue() != 0;
}
@Override
public WarehouseSummary loadSummary(String warehouseId) {
String sql = "SELECT COUNT(DISTINCT lane_code) AS lane_count, COUNT(*) AS space_count, "
+ "SUM(CASE WHEN space_status = '2' OR stock_detail_count > 0 THEN 1 ELSE 0 END) AS occupied_count, "
+ "SUM(CASE WHEN inbound_available = " + dialect.trueLiteral() + " THEN 1 ELSE 0 END) AS available_count, "
+ "COUNT(DISTINCT CASE WHEN pending_outbound = " + dialect.trueLiteral() + " THEN lane_code END) AS pending_lane_count, "
+ "SUM(CASE WHEN tray_conflict = " + dialect.trueLiteral() + " OR project_conflict = " + dialect.trueLiteral()
+ " OR product_conflict = " + dialect.trueLiteral() + " THEN 1 ELSE 0 END) AS conflict_count, "
+ "SUM(CASE WHEN reserved = " + dialect.trueLiteral() + " THEN 1 ELSE 0 END) AS reserved_count "
+ "FROM " + dialect.spaceStateView() + " WHERE warehouse_id = ?";
return jdbc.queryForObject(sql, (rs, row) -> new WarehouseSummary(warehouseId,
rs.getInt("lane_count"), rs.getInt("space_count"), rs.getInt("occupied_count"),
rs.getInt("available_count"), rs.getInt("pending_lane_count"), rs.getInt("conflict_count"),
rs.getInt("reserved_count")), warehouseId);
}
@Override
public LanePage pageCandidateLanes(String warehouseId, String afterLaneCode, int pageSize) {
return pageLanes(warehouseId, afterLaneCode, pageSize,
" AND available_count > 0 AND pending_outbound = " + dialect.falseLiteral()
+ " AND has_gap = " + dialect.falseLiteral());
}
@Override
public LanePage pageWarehouseLanes(String warehouseId, String afterLaneCode, int pageSize) {
return pageLanes(warehouseId, afterLaneCode, pageSize, "");
}
private LanePage pageLanes(String warehouseId, String afterLaneCode, int requestedSize, String hardFacts) {
int size = Math.max(1, Math.min(MAX_PAGE_SIZE, requestedSize));
String projection = "warehouse_id, lane_code, space_count, occupied_count, available_count, "
+ "pending_outbound, has_gap, state_version";
String sql = "SELECT TOP (:limit) " + projection + " FROM " + dialect.laneState()
+ " WHERE warehouse_id = :warehouse AND lane_code > :after" + hardFacts
+ " ORDER BY lane_code";
List<LaneCandidate> lanes = named.query(sql, new MapSqlParameterSource()
.addValue("warehouse", warehouseId).addValue("after", blankToEmpty(afterLaneCode))
.addValue("limit", size), laneCandidateMapper());
String next = lanes.size() == size ? lanes.get(lanes.size() - 1).laneCode() : null;
return new LanePage(lanes, next);
}
@Override
public List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes) {
LinkedHashSet<String> codes = laneCodes == null ? new LinkedHashSet<>()
: laneCodes.stream().filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
if (codes.isEmpty()) return List.of();
MapSqlParameterSource parameters = new MapSqlParameterSource()
.addValue("warehouse", warehouseId).addValue("lanes", codes);
Map<String, VersionToken> versions = new LinkedHashMap<>();
named.query("SELECT lane_code, state_version FROM " + dialect.laneState()
+ " WHERE warehouse_id = :warehouse AND lane_code IN (:lanes)", parameters,
(rs, row) -> Map.entry(rs.getString("lane_code"),
VersionToken.fromJdbc(rs.getObject("state_version"))))
.forEach(entry -> versions.put(entry.getKey(), entry.getValue()));
List<RawSpace> rawSpaces = named.query("SELECT space_id, warehouse_id, lane_code, area_id, zone_code, roadway_id, "
+ "platoon, column_no, floor_no, depth, space_status, locked, space_tray_code, role, "
+ "tray_conflict, project_conflict, product_conflict, pending_outbound, reserved, has_gap, inbound_available "
+ "FROM " + dialect.spaceStateView()
+ " WHERE warehouse_id = :warehouse AND lane_code IN (:lanes) "
+ "ORDER BY lane_code, depth, space_id", parameters, rawSpaceMapper());
List<InventoryItem> inventory = named.query("SELECT i.stock_id, i.space_id, i.tray_code, i.project_no, "
+ "i.product_no, i.lot_no, i.inbound_time_raw, i.inbound_at, i.quantity, i.out_quantity "
+ "FROM " + dialect.inventoryView() + " i JOIN " + dialect.spaceStateView() + " s ON s.space_id = i.space_id "
+ "WHERE s.warehouse_id = :warehouse AND s.lane_code IN (:lanes) "
+ "ORDER BY i.space_id, i.stock_id", parameters, inventoryMapper());
Map<String, List<InventoryItem>> inventoryBySpace = inventory.stream()
.collect(Collectors.groupingBy(InventoryItem::spaceId, LinkedHashMap::new, Collectors.toList()));
Map<String, List<RawSpace>> byLane = rawSpaces.stream()
.collect(Collectors.groupingBy(RawSpace::laneCode, LinkedHashMap::new, Collectors.toList()));
List<LaneSnapshot> result = new ArrayList<>();
byLane.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
int capacity = entry.getValue().size();
List<SpaceSnapshot> spaces = entry.getValue().stream()
.map(raw -> mapSpace(raw, capacity, versions.get(entry.getKey()),
inventoryBySpace.getOrDefault(raw.spaceId(), List.of())))
.sorted(Comparator.comparingInt((SpaceSnapshot value) -> value.state().depthOrder())
.thenComparing(SpaceSnapshot::spaceId)).toList();
result.add(new LaneSnapshot(warehouseId, entry.getKey(), versions.get(entry.getKey()), spaces));
});
return List.copyOf(result);
}
@Override
public List<SpaceSnapshot> loadRelocationBuffers(String warehouseId, Set<Integer> floors) {
if (floors == null || floors.isEmpty()) return List.of();
List<String> laneCodes = named.queryForList("SELECT DISTINCT lane_code FROM " + dialect.spaceStateView()
+ " WHERE warehouse_id = :warehouse AND role = 'RELOCATION_BUFFER' AND floor_no IN (:floors) "
+ "ORDER BY lane_code", new MapSqlParameterSource().addValue("warehouse", warehouseId)
.addValue("floors", floors), String.class);
return loadLaneSnapshots(warehouseId, laneCodes).stream().flatMap(lane -> lane.spaces().stream())
.filter(space -> space.state().role() == SpaceRole.RELOCATION_BUFFER)
.filter(space -> floors.contains(space.state().floorNo())).toList();
}
@Override
public Set<Integer> loadActiveDeepWorkflowFloors(String warehouseId, String excludingRequestId) {
String sql = "SELECT floor_no FROM " + dialect.deepWorkflowLock()
+ " WHERE warehouse_id = ? AND status = 'ACTIVE' AND (? IS NULL OR request_id <> ?) ORDER BY floor_no";
String excluded = blankToNull(excludingRequestId);
return new LinkedHashSet<>(jdbc.queryForList(sql, Integer.class, warehouseId, excluded, excluded));
}
@Override
public boolean acquireDeepWorkflowLock(String requestId, String warehouseId, int floorNo) {
return acquireDeepWorkflowLockResult(requestId, warehouseId, floorNo).changed();
}
@Override
public DeepWorkflowMutationResult acquireDeepWorkflowLockResult(String requestId, String warehouseId, int floorNo) {
if (!writesEnabled) return DeepWorkflowMutationResult.writesDisabled();
validateWorkflowLock(requestId, warehouseId, floorNo);
boolean acquired = dialect.h2Database() ? acquireDeepWorkflowH2(requestId, warehouseId, floorNo)
: named.queryForObject("EXEC " + dialect.acquireDeepWorkflowProcedure()
+ " @request_id=:request, @warehouse_id=:warehouse, @floor_no=:floor",
new MapSqlParameterSource().addValue("request", requestId).addValue("warehouse", warehouseId)
.addValue("floor", floorNo), (rs, row) -> rs.getBoolean("acquired"));
return acquired ? DeepWorkflowMutationResult.succeeded() : DeepWorkflowMutationResult.unchanged();
}
@Override
public boolean releaseDeepWorkflowLock(String requestId, String warehouseId, int floorNo) {
return releaseDeepWorkflowLockResult(requestId, warehouseId, floorNo).changed();
}
@Override
public DeepWorkflowMutationResult releaseDeepWorkflowLockResult(String requestId, String warehouseId, int floorNo) {
if (!writesEnabled) return DeepWorkflowMutationResult.writesDisabled();
validateWorkflowLock(requestId, warehouseId, floorNo);
boolean released = dialect.h2Database() ? releaseDeepWorkflowH2(requestId, warehouseId, floorNo)
: named.queryForObject("EXEC " + dialect.releaseDeepWorkflowProcedure()
+ " @request_id=:request, @warehouse_id=:warehouse, @floor_no=:floor",
new MapSqlParameterSource().addValue("request", requestId).addValue("warehouse", warehouseId)
.addValue("floor", floorNo), (rs, row) -> rs.getBoolean("released"));
return released ? DeepWorkflowMutationResult.succeeded() : DeepWorkflowMutationResult.unchanged();
}
@Override
public List<PendingOutboundJob> loadPendingOutboundJobs(String warehouseId) {
String sql = "SELECT job_id, task_no, tray_code, project_no, product_no, source_space_id, warehouse_id, source_lane_code, "
+ "source_resolution, status, in_or_out, create_time_raw FROM " + dialect.activeJobView()
+ " WHERE warehouse_id = ? AND in_or_out >= 20 ORDER BY job_id";
return jdbc.query(sql, (rs, row) -> new PendingOutboundJob(rs.getString("job_id"), rs.getString("task_no"),
rs.getString("tray_code"), rs.getString("project_no"), rs.getString("product_no"),
rs.getString("source_space_id"), rs.getString("warehouse_id"), rs.getString("source_lane_code"),
rs.getString("source_resolution"), rs.getString("status"), rs.getInt("in_or_out"),
rs.getString("create_time_raw")), warehouseId);
}
@Override
public List<OutboundStockMatch> searchOutboundStock(String warehouseId, OutboundStockSearch search) {
String predicate = search.trayNo() != null ? "i.tray_code = :tray"
: "i.project_no = :project AND (:product IS NULL OR i.product_no = :product)";
MapSqlParameterSource parameters = new MapSqlParameterSource().addValue("warehouse", warehouseId)
.addValue("tray", search.trayNo()).addValue("project", search.projectNo())
.addValue("product", blankToNull(search.productNo()));
String sql = "SELECT i.stock_id, i.space_id, s.lane_code, s.floor_no, i.tray_code, i.project_no, "
+ "i.product_no, i.lot_no, i.inbound_time_raw, i.inbound_at FROM " + dialect.inventoryView()
+ " i JOIN " + dialect.spaceStateView() + " s ON s.space_id = i.space_id "
+ "WHERE s.warehouse_id = :warehouse AND " + predicate
+ " ORDER BY i.inbound_at, i.inbound_time_raw, i.stock_id";
List<OutboundStockMatch> matches = named.query(sql, parameters, (rs, row) -> new OutboundStockMatch(
rs.getString("stock_id"), rs.getString("space_id"), rs.getString("lane_code"),
rs.getInt("floor_no"), rs.getString("tray_code"), rs.getString("project_no"),
rs.getString("product_no"), rs.getString("lot_no"), rs.getString("inbound_time_raw"),
instant(rs, "inbound_at")));
if (search.trayNo() == null) return matches;
List<OutboundStockMatch> cargoMatches = named.query("SELECT space_id, lane_code, floor_no, space_tray_code FROM "
+ dialect.spaceStateView() + " WHERE warehouse_id = :warehouse "
+ "AND LTRIM(RTRIM(space_tray_code)) = :tray ORDER BY space_id",
parameters, (rs, row) -> new OutboundStockMatch("CARGO_SPACE:" + rs.getString("space_id"),
rs.getString("space_id"), rs.getString("lane_code"), rs.getInt("floor_no"),
rs.getString("space_tray_code"), null, null, null, null, null));
Set<String> matchedSpaces = matches.stream().map(OutboundStockMatch::spaceId)
.collect(Collectors.toCollection(LinkedHashSet::new));
List<OutboundStockMatch> combined = new ArrayList<>(matches);
cargoMatches.stream().filter(match -> matchedSpaces.add(match.spaceId())).forEach(combined::add);
return List.copyOf(combined);
}
@Override
public int countProjectPallets(String warehouseId, String projectNo) {
String usableTray = "CASE WHEN s.stock_tray_count = 1 THEN NULLIF(LTRIM(RTRIM(i.tray_code)), '') "
+ "WHEN s.stock_tray_count = 0 THEN NULLIF(LTRIM(RTRIM(s.space_tray_code)), '') END";
Integer count = named.queryForObject("SELECT COUNT(DISTINCT " + usableTray + ") FROM "
+ dialect.spaceStateView() + " s JOIN " + dialect.inventoryView()
+ " i ON i.space_id = s.space_id WHERE s.warehouse_id = :warehouse "
+ "AND NULLIF(LTRIM(RTRIM(i.project_no)), '') = :project AND s.space_status = '2' "
+ "AND s.role = 'STORAGE' AND s.depth IS NOT NULL AND s.pending_outbound = "
+ dialect.falseLiteral() + " AND s.reserved = " + dialect.falseLiteral()
+ " AND s.has_gap = " + dialect.falseLiteral() + " AND s.tray_conflict = "
+ dialect.falseLiteral() + " AND s.project_conflict = " + dialect.falseLiteral()
+ " AND (NULLIF(LTRIM(RTRIM(s.locked)), '') IS NULL OR LTRIM(RTRIM(s.locked)) IN ('0', 'N')) "
+ "AND (NULLIF(LTRIM(RTRIM(s.space_tray_code)), '') IS NULL "
+ "OR NULLIF(LTRIM(RTRIM(i.tray_code)), '') IS NULL "
+ "OR LTRIM(RTRIM(s.space_tray_code)) = LTRIM(RTRIM(i.tray_code)))",
new MapSqlParameterSource().addValue("warehouse", warehouseId).addValue("project", projectNo),
Integer.class);
return count == null ? 0 : count;
}
@Override
public RepositoryPolicy loadPolicy(String warehouseId) {
List<Map<String, Object>> warehouses = jdbc.queryForList("SELECT target_fill_rate, fifo_tolerance_hours FROM "
+ dialect.warehousePolicy() + " WHERE warehouse_id = ?", warehouseId);
double targetFill = warehouses.isEmpty() || warehouses.get(0).get("TARGET_FILL_RATE") == null ? 0.0
: ((Number) warehouses.get(0).get("TARGET_FILL_RATE")).doubleValue();
int toleranceMinutes = warehouses.isEmpty() || warehouses.get(0).get("FIFO_TOLERANCE_HOURS") == null ? 0
: ((Number) warehouses.get(0).get("FIFO_TOLERANCE_HOURS")).intValue() * 60;
Set<String> isolated = new LinkedHashSet<>();
Set<String> common = new LinkedHashSet<>();
Set<String> mixedDenied = new LinkedHashSet<>();
jdbc.query("SELECT project_no, is_isolated, is_common, allow_mixed FROM " + dialect.projectPolicy(), rs -> {
if (rs.getBoolean("is_isolated")) isolated.add(rs.getString("project_no"));
if (rs.getBoolean("is_common")) common.add(rs.getString("project_no"));
if (!rs.getBoolean("allow_mixed")) mixedDenied.add(rs.getString("project_no"));
});
if (common.isEmpty()) common.add("COMMON");
SchedulingPolicy scheduling = SchedulingPolicy.ft546Defaults();
return new RepositoryPolicy(new InboundPolicy(scheduling, isolated, common, List.of(), 0, mixedDenied),
scheduling, toleranceMinutes, targetFill);
}
@Override
public DecisionLogEntry insertDecision(DecisionLogRecord record) {
String id = blankToNull(record.decisionId()) == null ? UUID.randomUUID().toString() : record.decisionId();
if (!writesEnabled) {
return new DecisionLogEntry(id, record.warehouseId(), record.decisionType(), record.inputVersion(),
record.resultCode(), abbreviate(record.reason(), 2000), Instant.now());
}
named.update("INSERT INTO " + dialect.decisionLog()
+ " (decision_id, warehouse_id, decision_type, input_version, result_code, reason) "
+ "VALUES (:id, :warehouse, :type, :version, :result, :reason)",
new MapSqlParameterSource().addValue("id", sqlId(id)).addValue("warehouse", record.warehouseId())
.addValue("type", record.decisionType()).addValue("version", versionValue(record.inputVersion()),
decisionVersionSqlType(dialect))
.addValue("result", record.resultCode()).addValue("reason", abbreviate(record.reason(), 2000)));
return findDecision(id).orElseThrow();
}
@Override
public Optional<DecisionLogEntry> findDecision(String decisionId) {
List<DecisionLogEntry> values = jdbc.query("SELECT decision_id, warehouse_id, decision_type, input_version, "
+ "result_code, reason, created_at FROM " + dialect.decisionLog() + " WHERE decision_id = ?",
(rs, row) -> new DecisionLogEntry(rs.getString("decision_id"), rs.getString("warehouse_id"),
rs.getString("decision_type"), VersionToken.fromJdbc(rs.getObject("input_version")),
rs.getString("result_code"), rs.getString("reason"), instant(rs, "created_at")), sqlId(decisionId));
return values.stream().findFirst();
}
@Override
public ReservationResult tryReserveInbound(InboundReservation reservation) {
if (!writesEnabled) return ReservationResult.conflict("WRITES_DISABLED", reservation.reservationId());
sqlId(reservation.reservationId());
if (reservation.expectedVersion() == null) {
return ReservationResult.conflict("LANE_VERSION_MISMATCH", reservation.reservationId());
}
return dialect.h2Database() ? reserveH2(reservation) : reserveSqlServer(reservation);
}
@Override
public ReservationResult releaseReservation(String warehouseId, String reservationId) {
if (!writesEnabled) return ReservationResult.conflict("WRITES_DISABLED", reservationId);
return dialect.h2Database() ? releaseH2(warehouseId, reservationId) : releaseSqlServer(warehouseId, reservationId);
}
private ReservationResult reserveH2(InboundReservation reservation) {
return transactions.execute(status -> {
List<LaneReservationState> values = jdbc.query("SELECT state_version, semantic_fingerprint FROM " + dialect.laneState()
+ " WHERE warehouse_id = ? AND lane_code = ? FOR UPDATE",
(rs, row) -> new LaneReservationState(VersionToken.fromJdbc(rs.getObject("state_version")),
rs.getString("semantic_fingerprint")),
reservation.warehouseId(), reservation.laneCode());
if (values.isEmpty() || !reservation.expectedVersion().equals(values.get(0).version())) {
return ReservationResult.conflict("LANE_VERSION_MISMATCH", reservation.reservationId());
}
String storedFingerprint = values.get(0).semanticFingerprint();
if (storedFingerprint != null && !storedFingerprint.isBlank()
&& !storedFingerprint.equals(h2SemanticFingerprint(
reservation.warehouseId(), reservation.laneCode(), InventoryFingerprintSource.LIVE))) {
return ReservationResult.conflict("LANE_VERSION_MISMATCH", reservation.reservationId());
}
Integer pending = jdbc.queryForObject("SELECT COUNT(*) FROM " + dialect.activeJobView()
+ " WHERE warehouse_id = ? AND in_or_out >= 20 AND source_lane_code = ?",
Integer.class, reservation.warehouseId(), reservation.laneCode());
if (pending != null && pending > 0) {
return ReservationResult.conflict("PENDING_OUTBOUND", reservation.reservationId());
}
Integer active = jdbc.queryForObject("SELECT COUNT(*) FROM " + dialect.reservation()
+ " WHERE space_id = ? AND status = 'ACTIVE'", Integer.class, reservation.spaceId());
if (active != null && active > 0) {
return ReservationResult.conflict("ACTIVE_RESERVATION_EXISTS", reservation.reservationId());
}
Integer eligible = jdbc.queryForObject("SELECT COUNT(*) FROM " + dialect.spaceStateView()
+ " WHERE warehouse_id = ? AND lane_code = ? AND space_id = ? AND inbound_available = TRUE",
Integer.class, reservation.warehouseId(), reservation.laneCode(), reservation.spaceId());
if (eligible == null || eligible == 0) {
return ReservationResult.conflict("SPACE_NOT_ELIGIBLE", reservation.reservationId());
}
try {
jdbc.update("INSERT INTO " + dialect.reservation()
+ " (reservation_id, warehouse_id, lane_code, space_id, project_no, status) "
+ "VALUES (?, ?, ?, ?, ?, 'ACTIVE')", sqlId(reservation.reservationId()),
reservation.warehouseId(), reservation.laneCode(), reservation.spaceId(), reservation.projectNo());
} catch (DuplicateKeyException exception) {
return ReservationResult.conflict("ACTIVE_RESERVATION_EXISTS", reservation.reservationId());
}
rebuildH2Lane(reservation.warehouseId(), reservation.laneCode());
return ReservationResult.reserved(reservation.reservationId());
});
}
private boolean acquireDeepWorkflowH2(String requestId, String warehouseId, int floorNo) {
return Boolean.TRUE.equals(transactions.execute(status -> {
List<String> owners = jdbc.query("SELECT request_id FROM " + dialect.deepWorkflowLock()
+ " WHERE warehouse_id = ? AND floor_no = ? AND status = 'ACTIVE' FOR UPDATE",
(rs, row) -> rs.getString("request_id"), warehouseId, floorNo);
if (!owners.isEmpty()) return requestId.equals(owners.get(0));
try {
jdbc.update("INSERT INTO " + dialect.deepWorkflowLock()
+ " (lock_id, request_id, warehouse_id, floor_no, status) VALUES (?, ?, ?, ?, 'ACTIVE')",
UUID.randomUUID().toString(), requestId, warehouseId, floorNo);
return true;
} catch (DuplicateKeyException exception) {
return false;
}
}));
}
private boolean releaseDeepWorkflowH2(String requestId, String warehouseId, int floorNo) {
return Boolean.TRUE.equals(transactions.execute(status -> jdbc.update("UPDATE " + dialect.deepWorkflowLock()
+ " SET status = 'RELEASED', released_at = CURRENT_TIMESTAMP WHERE request_id = ? "
+ "AND warehouse_id = ? AND floor_no = ? AND status = 'ACTIVE'",
requestId, warehouseId, floorNo) == 1));
}
private ReservationResult releaseH2(String warehouseId, String reservationId) {
return transactions.execute(status -> {
Object reservationKey = sqlId(reservationId);
List<Map<String, Object>> rows = jdbc.queryForList("SELECT warehouse_id, lane_code FROM "
+ dialect.reservation() + " WHERE reservation_id = ? AND warehouse_id = ? AND status = 'ACTIVE' FOR UPDATE",
reservationKey, warehouseId);
if (rows.isEmpty()) return ReservationResult.notFound(reservationId);
jdbc.update("UPDATE " + dialect.reservation()
+ " SET status = 'RELEASED', released_at = CURRENT_TIMESTAMP WHERE reservation_id = ? AND warehouse_id = ?",
reservationKey, warehouseId);
String warehouse = Objects.toString(rows.get(0).get("WAREHOUSE_ID"), null);
String lane = Objects.toString(rows.get(0).get("LANE_CODE"), null);
rebuildH2Lane(warehouse, lane);
return ReservationResult.released(reservationId);
});
}
private void rebuildH2Lane(String warehouseId, String laneCode) {
String fingerprint = h2SemanticFingerprint(warehouseId, laneCode,
InventoryFingerprintSource.SNAPSHOT);
jdbc.update("UPDATE " + dialect.laneState() + " SET "
+ "space_count = (SELECT COUNT(*) FROM " + dialect.spaceStateView() + " WHERE warehouse_id = ? AND lane_code = ?), "
+ "occupied_count = (SELECT COUNT(*) FROM " + dialect.spaceStateView() + " WHERE warehouse_id = ? AND lane_code = ? AND (space_status = '2' OR stock_detail_count > 0)), "
+ "available_count = (SELECT COUNT(*) FROM " + dialect.spaceStateView() + " WHERE warehouse_id = ? AND lane_code = ? AND inbound_available = TRUE), "
+ "pending_outbound = CASE WHEN EXISTS (SELECT 1 FROM " + dialect.spaceStateView() + " WHERE warehouse_id = ? AND lane_code = ? AND pending_outbound = TRUE) THEN TRUE ELSE FALSE END, "
+ "has_gap = CASE WHEN EXISTS (SELECT 1 FROM " + dialect.spaceStateView() + " WHERE warehouse_id = ? AND lane_code = ? AND has_gap = TRUE) THEN TRUE ELSE FALSE END, "
+ "semantic_fingerprint = ?, state_version = state_version + 1, rebuilt_at = CURRENT_TIMESTAMP "
+ "WHERE warehouse_id = ? AND lane_code = ?",
warehouseId, laneCode, warehouseId, laneCode, warehouseId, laneCode,
warehouseId, laneCode, warehouseId, laneCode, fingerprint, warehouseId, laneCode);
}
private ReservationResult reserveSqlServer(InboundReservation reservation) {
String sql = "EXEC " + dialect.tryReserveProcedure()
+ " @warehouse_id=:warehouse, @lane_code=:lane, @space_id=:space, @project_no=:project, "
+ "@expected_state_version=:version, @reservation_id=:reservation";
return named.queryForObject(sql, new MapSqlParameterSource().addValue("warehouse", reservation.warehouseId())
.addValue("lane", reservation.laneCode()).addValue("space", reservation.spaceId())
.addValue("project", reservation.projectNo()).addValue("version", versionValue(reservation.expectedVersion()))
.addValue("reservation", sqlId(reservation.reservationId())), (rs, row) -> reservationResult(rs));
}
private ReservationResult releaseSqlServer(String warehouseId, String reservationId) {
return named.queryForObject("EXEC " + dialect.releaseProcedure()
+ " @warehouse_id=:warehouse, @reservation_id=:reservation",
new MapSqlParameterSource().addValue("warehouse", warehouseId)
.addValue("reservation", sqlId(reservationId)), (rs, row) -> reservationResult(rs));
}
private ReservationResult reservationResult(ResultSet rs) throws SQLException {
String result = rs.getString("result");
String id = rs.getString("reservation_id");
return switch (result) {
case "RESERVED" -> ReservationResult.reserved(id);
case "RELEASED" -> ReservationResult.released(id);
case "NOT_FOUND" -> ReservationResult.notFound(id);
default -> ReservationResult.conflict(rs.getString("reason"), id);
};
}
private SpaceSnapshot mapSpace(RawSpace raw, int capacity, VersionToken version, List<InventoryItem> inventory) {
EnumSet<SpaceConflict> conflicts = EnumSet.noneOf(SpaceConflict.class);
if (raw.trayConflict()) conflicts.add(SpaceConflict.TRAY);
if (raw.projectConflict()) conflicts.add(SpaceConflict.PROJECT);
String inventoryTray = unique(inventory, InventoryItem::trayNo);
String tray = blankToNull(raw.spaceTrayNo()) != null ? raw.spaceTrayNo() : inventoryTray;
if (blankToNull(raw.spaceTrayNo()) != null && inventoryTray != null && !raw.spaceTrayNo().equals(inventoryTray)) {
conflicts.add(SpaceConflict.SPACE_TRAY_MISMATCH);
}
String project = unique(inventory, InventoryItem::projectNo);
String product = unique(inventory, InventoryItem::productNo);
String lot = unique(inventory, InventoryItem::lotNo);
Instant inboundAt = inventory.stream().map(InventoryItem::inboundAt).filter(Objects::nonNull)
.min(Comparator.naturalOrder()).orElse(null);
boolean invalid = locked(raw.locked()) || raw.reserved() || raw.hasGap() || raw.depth() == null
|| !conflicts.isEmpty() || ("1".equals(raw.spaceStatus()) && (!inventory.isEmpty() || tray != null));
SpaceStatus status = invalid ? SpaceStatus.DISABLED : switch (Objects.toString(raw.spaceStatus(), "")) {
case "1" -> SpaceStatus.EMPTY;
case "2" -> SpaceStatus.OCCUPIED;
default -> SpaceStatus.DISABLED;
};
SpaceState state = new SpaceState(raw.spaceId(), raw.zoneCode(), raw.laneCode(),
raw.depth() == null ? 0 : raw.depth(), capacity, raw.floorNo(), raw.columnNo(),
raw.depth() == null ? 0 : raw.depth(), raw.platoon() == null ? null : raw.platoon().toString(),
mapRole(raw.role()), status,
raw.pendingOutbound(), !conflicts.isEmpty(), tray, project, product, lot, inboundAt, null,
raw.reserved(), version == null ? null : version.externalForm(), !inventory.isEmpty());
String diagnostic = invalid ? diagnostic(raw, conflicts) : null;
return new SpaceSnapshot(state, inventory, conflicts, raw.inboundAvailable(), raw.hasGap(), diagnostic);
}
private String diagnostic(RawSpace raw, Set<SpaceConflict> conflicts) {
if (!conflicts.isEmpty()) return "INVENTORY_CONFLICT:" + conflicts;
if (raw.reserved()) return "RESERVED";
if (raw.hasGap()) return "LANE_GAP";
if (raw.depth() == null) return "INVALID_TOPOLOGY";
if (locked(raw.locked())) return "LOCKED";
return "NON_AVAILABLE_STATUS:" + raw.spaceStatus();
}
private SpaceRole mapRole(String role) {
return switch (role) {
case "STORAGE" -> SpaceRole.STORAGE;
case "RELOCATION_BUFFER" -> SpaceRole.RELOCATION_BUFFER;
case "OUTBOUND_BUFFER" -> SpaceRole.OUTBOUND_CACHE;
case "PROTECTED_CORE" -> SpaceRole.PROTECTED_CORE;
default -> throw new IllegalStateException("Unknown space role: " + role);
};
}
private RowMapper<LaneCandidate> laneCandidateMapper() {
return (rs, row) -> new LaneCandidate(rs.getString("warehouse_id"), rs.getString("lane_code"),
rs.getInt("space_count"), rs.getInt("occupied_count"), rs.getInt("available_count"),
rs.getBoolean("pending_outbound"), rs.getBoolean("has_gap"),
VersionToken.fromJdbc(rs.getObject("state_version")));
}
private RowMapper<RawSpace> rawSpaceMapper() {
return (rs, row) -> new RawSpace(rs.getString("space_id"), rs.getString("lane_code"),
rs.getString("area_id"), rs.getString("zone_code"), rs.getString("roadway_id"),
nullableInt(rs, "platoon"), rs.getInt("column_no"), rs.getInt("floor_no"),
nullableInt(rs, "depth"), rs.getString("space_status"),
rs.getString("locked"), rs.getString("space_tray_code"), rs.getString("role"),
rs.getBoolean("tray_conflict"), rs.getBoolean("project_conflict"),
rs.getBoolean("product_conflict"), rs.getBoolean("pending_outbound"),
rs.getBoolean("reserved"), rs.getBoolean("has_gap"), rs.getBoolean("inbound_available"));
}
private RowMapper<InventoryItem> inventoryMapper() {
return (rs, row) -> new InventoryItem(rs.getString("stock_id"), rs.getString("space_id"),
rs.getString("tray_code"), rs.getString("project_no"), rs.getString("product_no"),
rs.getString("lot_no"), rs.getString("inbound_time_raw"), instant(rs, "inbound_at"),
rs.getBigDecimal("quantity"), rs.getBigDecimal("out_quantity"));
}
private Object sqlId(String id) {
UUID value = UUID.fromString(id);
return dialect.h2Database() ? value.toString() : value;
}
private void validateWorkflowLock(String requestId, String warehouseId, int floorNo) {
if (blankToNull(requestId) == null) throw new IllegalArgumentException("requestId is required");
if (blankToNull(warehouseId) == null) throw new IllegalArgumentException("warehouseId is required");
if (floorNo <= 0) throw new IllegalArgumentException("floorNo must be positive");
}
private Object versionValue(VersionToken token) { return token == null ? null : token.jdbcValue(); }
static int decisionVersionSqlType(RepositoryDialect dialect) {
return dialect.h2Database() ? Types.BIGINT : Types.BINARY;
}
private String abbreviate(String value, int maximum) {
return value == null || value.length() <= maximum ? value : value.substring(0, maximum);
}
private boolean locked(String value) {
String normalized = blankToNull(value);
return normalized != null && !"0".equalsIgnoreCase(normalized) && !"N".equalsIgnoreCase(normalized);
}
private String blankToEmpty(String value) { return value == null ? "" : value; }
private String blankToNull(String value) { return value == null || value.isBlank() ? null : value; }
private Integer nullableInt(ResultSet rs, String column) throws SQLException {
int value = rs.getInt(column);
return rs.wasNull() ? null : value;
}
private Instant instant(ResultSet rs, String column) throws SQLException {
Timestamp timestamp = rs.getTimestamp(column);
return timestamp == null ? null : timestamp.toInstant();
}
private <T> String unique(List<T> values, Function<T, String> extractor) {
Set<String> distinct = values.stream().map(extractor).map(this::blankToNull)
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
return distinct.size() == 1 ? distinct.iterator().next() : null;
}
private <T> long distinctCount(List<T> values, Function<T, String> extractor) {
return values.stream().map(extractor).map(this::blankToNull).filter(Objects::nonNull).distinct().count();
}
private record RawSpace(String spaceId, String laneCode, String areaId, String zoneCode, String roadwayId,
Integer platoon, int columnNo, int floorNo, Integer depth, String spaceStatus,
String locked, String spaceTrayNo,
String role, boolean trayConflict, boolean projectConflict, boolean productConflict,
boolean pendingOutbound, boolean reserved, boolean hasGap, boolean inboundAvailable) {
}
private record LaneReservationState(VersionToken version, String semanticFingerprint) {
}
private enum InventoryFingerprintSource {
SNAPSHOT,
LIVE
}
}

View File

@@ -0,0 +1,5 @@
package com.ruoyi.algorithm.repository;
public record LaneCandidate(String warehouseId, String laneCode, int spaceCount, int occupiedCount,
int availableCount, boolean pendingOutbound, boolean hasGap, VersionToken version) {
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.repository;
import java.util.List;
public record LanePage(List<LaneCandidate> lanes, String nextLaneCode) {
public LanePage { lanes = List.copyOf(lanes); }
}

View File

@@ -0,0 +1,10 @@
package com.ruoyi.algorithm.repository;
import com.ruoyi.algorithm.model.SpaceState;
import java.util.List;
public record LaneSnapshot(String warehouseId, String laneCode, VersionToken version, List<SpaceSnapshot> spaces) {
public LaneSnapshot { spaces = List.copyOf(spaces); }
public List<SpaceState> spaceStates() { return spaces.stream().map(SpaceSnapshot::state).toList(); }
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.algorithm.repository;
import java.time.Instant;
public record OutboundStockMatch(String stockId, String spaceId, String laneCode, int floorNo, String trayNo,
String projectNo, String productNo, String lotNo,
String inboundTimeRaw, Instant inboundAt) {
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.algorithm.repository;
public record OutboundStockSearch(String trayNo, String projectNo, String productNo) {
public OutboundStockSearch {
boolean hasTray = trayNo != null && !trayNo.isBlank();
boolean hasProject = projectNo != null && !projectNo.isBlank();
if (hasTray == hasProject) throw new IllegalArgumentException("Specify exact tray or project, but not both");
}
public static OutboundStockSearch exactTray(String trayNo) { return new OutboundStockSearch(trayNo, null, null); }
public static OutboundStockSearch project(String projectNo, String productNo) {
return new OutboundStockSearch(null, projectNo, productNo);
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.repository;
public record PendingOutboundJob(String jobId, String taskNo, String trayNo, String projectNo,
String productNo, String sourceSpaceId, String warehouseId,
String sourceLaneCode, String sourceResolution, String status,
int direction, String createTimeRaw) {
}

View File

@@ -0,0 +1,74 @@
package com.ruoyi.algorithm.repository;
public final class RepositoryDialect {
private final boolean h2Database;
private final String laneState;
private final String spaceStateView;
private final String inventoryView;
private final String activeJobView;
private final String warehousePolicy;
private final String projectPolicy;
private final String reservation;
private final String decisionLog;
private final String deepWorkflowLock;
private final String tryReserveProcedure;
private final String releaseProcedure;
private final String acquireDeepWorkflowProcedure;
private final String releaseDeepWorkflowProcedure;
private final String trueLiteral;
private final String falseLiteral;
private RepositoryDialect(boolean h2Database, String laneState, String spaceStateView, String inventoryView,
String activeJobView, String warehousePolicy, String projectPolicy,
String reservation, String decisionLog, String deepWorkflowLock,
String tryReserveProcedure, String releaseProcedure,
String acquireDeepWorkflowProcedure, String releaseDeepWorkflowProcedure,
String trueLiteral, String falseLiteral) {
this.h2Database = h2Database;
this.laneState = laneState;
this.spaceStateView = spaceStateView;
this.inventoryView = inventoryView;
this.activeJobView = activeJobView;
this.warehousePolicy = warehousePolicy;
this.projectPolicy = projectPolicy;
this.reservation = reservation;
this.decisionLog = decisionLog;
this.deepWorkflowLock = deepWorkflowLock;
this.tryReserveProcedure = tryReserveProcedure;
this.releaseProcedure = releaseProcedure;
this.acquireDeepWorkflowProcedure = acquireDeepWorkflowProcedure;
this.releaseDeepWorkflowProcedure = releaseDeepWorkflowProcedure;
this.trueLiteral = trueLiteral;
this.falseLiteral = falseLiteral;
}
public static RepositoryDialect h2() {
return new RepositoryDialect(true, "ALG_LANE_STATE", "ALG_V_SPACE_STATE", "ALG_V_INVENTORY_ITEM",
"ALG_V_ACTIVE_JOB", "ALG_WAREHOUSE_POLICY", "ALG_PROJECT_POLICY", "ALG_RESERVATION",
"ALG_DECISION_LOG", "ALG_DEEP_WORKFLOW_LOCK", null, null, null, null, "TRUE", "FALSE");
}
public static RepositoryDialect sqlServer() {
return new RepositoryDialect(false, "alg.lane_state", "alg.v_space_state", "alg.v_inventory_item",
"alg.v_active_job", "alg.warehouse_policy", "alg.project_policy", "alg.reservation",
"alg.decision_log", "alg.deep_workflow_lock", "alg.usp_try_reserve_inbound",
"alg.usp_release_reservation", "alg.usp_acquire_deep_workflow_lock",
"alg.usp_release_deep_workflow_lock", "1", "0");
}
public boolean h2Database() { return h2Database; }
public String laneState() { return laneState; }
public String spaceStateView() { return spaceStateView; }
public String inventoryView() { return inventoryView; }
public String activeJobView() { return activeJobView; }
public String warehousePolicy() { return warehousePolicy; }
public String projectPolicy() { return projectPolicy; }
public String reservation() { return reservation; }
public String decisionLog() { return decisionLog; }
public String deepWorkflowLock() { return deepWorkflowLock; }
public String tryReserveProcedure() { return tryReserveProcedure; }
public String releaseProcedure() { return releaseProcedure; }
public String acquireDeepWorkflowProcedure() { return acquireDeepWorkflowProcedure; }
public String releaseDeepWorkflowProcedure() { return releaseDeepWorkflowProcedure; }
public String trueLiteral() { return trueLiteral; }
public String falseLiteral() { return falseLiteral; }
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.algorithm.repository;
import com.ruoyi.algorithm.inbound.InboundPolicy;
import com.ruoyi.algorithm.policy.SchedulingPolicy;
public record RepositoryPolicy(InboundPolicy inboundPolicy, SchedulingPolicy schedulingPolicy,
int fifoToleranceMinutes, double targetFillRate) {
public static RepositoryPolicy defaults() {
return new RepositoryPolicy(InboundPolicy.defaults(), SchedulingPolicy.ft546Defaults(), 0, 0.0);
}
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.algorithm.repository;
public record ReservationResult(ReservationStatus status, String reasonCode, String reservationId) {
public static ReservationResult reserved(String id) { return new ReservationResult(ReservationStatus.RESERVED, null, id); }
public static ReservationResult conflict(String reason, String id) { return new ReservationResult(ReservationStatus.CONFLICT, reason, id); }
public static ReservationResult released(String id) { return new ReservationResult(ReservationStatus.RELEASED, null, id); }
public static ReservationResult notFound(String id) { return new ReservationResult(ReservationStatus.NOT_FOUND, "NOT_FOUND", id); }
}

View File

@@ -0,0 +1,3 @@
package com.ruoyi.algorithm.repository;
public enum ReservationStatus { RESERVED, CONFLICT, RELEASED, NOT_FOUND }

View File

@@ -0,0 +1,3 @@
package com.ruoyi.algorithm.repository;
public enum SpaceConflict { TRAY, PROJECT, PRODUCT, LOT, SPACE_TRAY_MISMATCH }

View File

@@ -0,0 +1,15 @@
package com.ruoyi.algorithm.repository;
import com.ruoyi.algorithm.model.SpaceState;
import java.util.List;
import java.util.Set;
public record SpaceSnapshot(SpaceState state, List<InventoryItem> inventory, Set<SpaceConflict> conflicts,
boolean inboundAvailable, boolean hasGap, String diagnostic) {
public SpaceSnapshot {
inventory = List.copyOf(inventory);
conflicts = Set.copyOf(conflicts);
}
public String spaceId() { return state.spaceId(); }
}

View File

@@ -0,0 +1,50 @@
package com.ruoyi.algorithm.repository;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
public final class VersionToken {
public enum Kind { NUMERIC, BINARY }
private final Kind kind;
private final Long numeric;
private final byte[] binary;
private VersionToken(Kind kind, Long numeric, byte[] binary) {
this.kind = kind;
this.numeric = numeric;
this.binary = binary == null ? null : binary.clone();
}
public static VersionToken numeric(long value) {
return new VersionToken(Kind.NUMERIC, value, null);
}
public static VersionToken binary(byte[] value) {
Objects.requireNonNull(value, "value");
return new VersionToken(Kind.BINARY, null, value);
}
public static VersionToken fromJdbc(Object value) {
if (value == null) return null;
if (value instanceof byte[] bytes) return binary(bytes);
if (value instanceof Number number) return numeric(number.longValue());
throw new IllegalArgumentException("Unsupported version token type: " + value.getClass().getName());
}
public Kind kind() { return kind; }
public Object jdbcValue() { return kind == Kind.BINARY ? binary.clone() : numeric; }
public String externalForm() {
return kind == Kind.BINARY ? Base64.getEncoder().encodeToString(binary) : Long.toString(numeric);
}
@Override public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof VersionToken token) || kind != token.kind) return false;
return Objects.equals(numeric, token.numeric) && Arrays.equals(binary, token.binary);
}
@Override public int hashCode() { return 31 * Objects.hash(kind, numeric) + Arrays.hashCode(binary); }
@Override public String toString() { return externalForm(); }
}

View File

@@ -0,0 +1,39 @@
package com.ruoyi.algorithm.repository;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public interface WarehouseRepository {
void synchronizeReadModel(String warehouseId);
default void refreshChangedLanes(String warehouseId) {
synchronizeReadModel(warehouseId);
}
WarehouseSummary loadSummary(String warehouseId);
LanePage pageCandidateLanes(String warehouseId, String afterLaneCode, int pageSize);
LanePage pageWarehouseLanes(String warehouseId, String afterLaneCode, int pageSize);
List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes);
List<SpaceSnapshot> loadRelocationBuffers(String warehouseId, Set<Integer> floors);
Set<Integer> loadActiveDeepWorkflowFloors(String warehouseId, String excludingRequestId);
boolean acquireDeepWorkflowLock(String requestId, String warehouseId, int floorNo);
boolean releaseDeepWorkflowLock(String requestId, String warehouseId, int floorNo);
default DeepWorkflowMutationResult acquireDeepWorkflowLockResult(String requestId, String warehouseId, int floorNo) {
return acquireDeepWorkflowLock(requestId, warehouseId, floorNo)
? DeepWorkflowMutationResult.succeeded() : DeepWorkflowMutationResult.unchanged();
}
default DeepWorkflowMutationResult releaseDeepWorkflowLockResult(String requestId, String warehouseId, int floorNo) {
return releaseDeepWorkflowLock(requestId, warehouseId, floorNo)
? DeepWorkflowMutationResult.succeeded() : DeepWorkflowMutationResult.unchanged();
}
List<PendingOutboundJob> loadPendingOutboundJobs(String warehouseId);
List<OutboundStockMatch> searchOutboundStock(String warehouseId, OutboundStockSearch search);
int countProjectPallets(String warehouseId, String projectNo);
RepositoryPolicy loadPolicy(String warehouseId);
DecisionLogEntry insertDecision(DecisionLogRecord record);
Optional<DecisionLogEntry> findDecision(String decisionId);
ReservationResult tryReserveInbound(InboundReservation reservation);
ReservationResult releaseReservation(String warehouseId, String reservationId);
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.algorithm.repository;
public record WarehouseSummary(String warehouseId, int laneCount, int spaceCount, int occupiedCount,
int availableCount, int pendingLaneCount, int conflictSpaceCount,
int reservedSpaceCount) {
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.service;
import com.ruoyi.algorithm.inbound.InboundPlan;
import com.ruoyi.algorithm.repository.VersionToken;
public record InboundPlanningResult(InboundPlan plan, VersionToken laneVersion) {
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.algorithm.service;
import com.ruoyi.algorithm.repository.ReservationResult;
public record InboundReservationOutcome(String resultCode, ReservationResult reservation) {
}

View File

@@ -0,0 +1,295 @@
package com.ruoyi.algorithm.service;
import com.ruoyi.algorithm.inbound.InboundContext;
import com.ruoyi.algorithm.inbound.InboundPlan;
import com.ruoyi.algorithm.inbound.InboundPlanner;
import com.ruoyi.algorithm.inbound.InboundRequest;
import com.ruoyi.algorithm.inbound.PendingOutbound;
import com.ruoyi.algorithm.model.SpaceState;
import com.ruoyi.algorithm.outbound.OutboundCapacityMode;
import com.ruoyi.algorithm.outbound.OutboundContext;
import com.ruoyi.algorithm.outbound.OutboundError;
import com.ruoyi.algorithm.outbound.OutboundErrorCode;
import com.ruoyi.algorithm.outbound.OutboundPlan;
import com.ruoyi.algorithm.outbound.OutboundPlanner;
import com.ruoyi.algorithm.outbound.OutboundRequest;
import com.ruoyi.algorithm.tidy.TidyPendingOutbound;
import com.ruoyi.algorithm.tidy.TidyPlan;
import com.ruoyi.algorithm.tidy.TidyPlanner;
import com.ruoyi.algorithm.tidy.TidyRequest;
import com.ruoyi.algorithm.repository.DecisionLogRecord;
import com.ruoyi.algorithm.repository.DeepWorkflowMutationResult;
import com.ruoyi.algorithm.repository.InboundReservation;
import com.ruoyi.algorithm.repository.LaneCandidate;
import com.ruoyi.algorithm.repository.LanePage;
import com.ruoyi.algorithm.repository.LaneSnapshot;
import com.ruoyi.algorithm.repository.OutboundStockMatch;
import com.ruoyi.algorithm.repository.OutboundStockSearch;
import com.ruoyi.algorithm.repository.PendingOutboundJob;
import com.ruoyi.algorithm.repository.RepositoryPolicy;
import com.ruoyi.algorithm.repository.ReservationResult;
import com.ruoyi.algorithm.repository.ReservationStatus;
import com.ruoyi.algorithm.repository.SpaceSnapshot;
import com.ruoyi.algorithm.repository.VersionToken;
import com.ruoyi.algorithm.repository.WarehouseRepository;
import com.ruoyi.algorithm.repository.WarehouseSummary;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public final class WarehouseApplicationService {
private final WarehouseRepository repository;
private final InboundPlanner inboundPlanner;
private final OutboundPlanner outboundPlanner;
private final TidyPlanner tidyPlanner;
private final int pageSize;
public WarehouseApplicationService(WarehouseRepository repository, InboundPlanner inboundPlanner,
OutboundPlanner outboundPlanner, TidyPlanner tidyPlanner, int pageSize) {
this.repository = Objects.requireNonNull(repository, "repository");
this.inboundPlanner = Objects.requireNonNull(inboundPlanner, "inboundPlanner");
this.outboundPlanner = Objects.requireNonNull(outboundPlanner, "outboundPlanner");
this.tidyPlanner = Objects.requireNonNull(tidyPlanner, "tidyPlanner");
this.pageSize = Math.max(1, Math.min(200, pageSize));
}
public WarehouseSummary summary(String warehouseId) {
return repository.loadSummary(warehouseId);
}
public WarehouseDiagnostics diagnostics(String warehouseId) {
WarehouseSummary summary = repository.loadSummary(warehouseId);
List<String> issues = new ArrayList<>();
forEachLanePage(warehouseId, false, snapshots -> snapshots.stream()
.flatMap(lane -> lane.spaces().stream())
.filter(space -> space.diagnostic() != null)
.forEach(space -> issues.add(space.spaceId() + ":" + space.diagnostic())));
return new WarehouseDiagnostics(summary, issues.stream().sorted().toList());
}
public InboundPlanningResult planInbound(String warehouseId, InboundRequest request) {
repository.refreshChangedLanes(warehouseId);
RepositoryPolicy policy = repository.loadPolicy(warehouseId);
WarehouseSummary summary = repository.loadSummary(warehouseId);
InboundRequest effectiveRequest = request;
if (request.expectedProjectPallets() == null && nonBlank(request.projectNo())) {
int currentProjectPallets = repository.countProjectPallets(warehouseId, request.projectNo());
effectiveRequest = new InboundRequest(request.requestId(), request.trayNo(), request.projectNo(),
request.productId(), request.lotNo(), currentProjectPallets <= 1 ? 2 : 4,
request.entryFloor(), request.targetFloor(), request.allowedFloors(), request.batchLaneMode());
}
List<PendingOutbound> pending = repository.loadPendingOutboundJobs(warehouseId).stream()
.map(job -> new PendingOutbound(job.projectNo(), job.productNo(), job.trayNo(),
job.sourceLaneCode(), job.sourceResolution())).toList();
InboundPlanner.PagedSession session = inboundPlanner.startPaged(effectiveRequest,
policy.inboundPolicy(), pending, summary.occupiedCount());
Map<String, VersionToken> versions = new LinkedHashMap<>();
String cursor = null;
do {
LanePage page = repository.pageCandidateLanes(warehouseId, cursor, pageSize);
List<String> laneCodes = page.lanes().stream().map(LaneCandidate::laneCode).toList();
if (!laneCodes.isEmpty()) {
List<LaneSnapshot> snapshots = repository.loadLaneSnapshots(warehouseId, laneCodes);
snapshots.forEach(lane -> versions.put(lane.laneCode(), lane.version()));
session.accept(snapshots.stream().flatMap(lane -> lane.spaceStates().stream()).toList());
versions.keySet().retainAll(session.retainedLaneCodes());
}
cursor = page.nextLaneCode();
} while (cursor != null);
InboundPlan plan = session.finish();
VersionToken version = plan.assignment().map(value -> versions.get(value.laneCode())).orElse(null);
String resultCode = plan.assignment().isPresent() ? "READY"
: "REJECTED_" + plan.rejection().orElseThrow().code().name();
String reason = plan.assignment().map(value -> value.reason())
.orElseGet(() -> plan.rejection().orElseThrow().reason());
log(warehouseId, "INBOUND", version, resultCode, reason);
return new InboundPlanningResult(plan, version);
}
public OutboundPlan planOutbound(String warehouseId, OutboundRequest request) {
repository.refreshChangedLanes(warehouseId);
RepositoryPolicy policy = repository.loadPolicy(warehouseId);
WarehouseSummary summary = repository.loadSummary(warehouseId);
OutboundRequest effectiveRequest = request.fifoToleranceMinutes() == null && policy.fifoToleranceMinutes() > 0
? new OutboundRequest(request.requestId(), request.trayNo(), request.projectNo(), request.productId(),
request.fifoMode(), policy.fifoToleranceMinutes(), request.exitFloor(), request.wholeTray())
: request;
List<OutboundStockMatch> matches = new ArrayList<>();
String matchedProductScope = null;
if (nonBlank(request.trayNo())) {
matches.addAll(repository.searchOutboundStock(warehouseId, OutboundStockSearch.exactTray(request.trayNo())));
boolean fifoEnabled = nonBlank(request.fifoMode()) && !"OFF".equalsIgnoreCase(request.fifoMode());
Set<String> spaces = matches.stream().map(OutboundStockMatch::spaceId).filter(this::nonBlank)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
Set<String> projects = matches.stream().map(OutboundStockMatch::projectNo).filter(this::nonBlank)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
Set<String> products = matches.stream().map(OutboundStockMatch::productNo).filter(this::nonBlank)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
if (spaces.size() > 1 || projects.size() > 1) {
OutboundPlan ambiguous = OutboundPlan.error(new OutboundError(OutboundErrorCode.AMBIGUOUS_TRAY,
"Exact tray resolves to multiple spaces or inventory scopes.",
Map.of("spaceCount", spaces.size(), "projectCount", projects.size(),
"productCount", products.size())));
log(warehouseId, "OUTBOUND", null, "ERROR_AMBIGUOUS_TRAY",
ambiguous.error().orElseThrow().message());
return ambiguous;
}
if (fifoEnabled && projects.size() == 1) {
String project = projects.iterator().next();
String product = products.size() == 1 ? products.iterator().next() : null;
matchedProductScope = product;
List<OutboundStockMatch> scoped = repository.searchOutboundStock(warehouseId,
OutboundStockSearch.project(project, product));
scoped.forEach(match -> {
if (matches.stream().noneMatch(existing -> existing.stockId().equals(match.stockId()))) {
matches.add(match);
}
});
}
} else if (nonBlank(request.projectNo())) {
matchedProductScope = nonBlank(request.productId()) ? request.productId() : null;
matches.addAll(repository.searchOutboundStock(warehouseId,
OutboundStockSearch.project(request.projectNo(), request.productId())));
}
List<String> laneCodes = matches.stream().map(OutboundStockMatch::laneCode).filter(Objects::nonNull)
.distinct().sorted().toList();
List<LaneSnapshot> lanes = repository.loadLaneSnapshots(warehouseId, laneCodes);
Set<Integer> floors = matches.stream().map(OutboundStockMatch::floorNo)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
List<SpaceSnapshot> buffers = repository.loadRelocationBuffers(warehouseId, floors);
Map<String, SpaceState> uniqueSpaces = new LinkedHashMap<>();
Set<String> matchedSpaceIds = matches.stream().map(OutboundStockMatch::spaceId)
.filter(this::nonBlank).collect(java.util.stream.Collectors.toSet());
String productScope = matchedProductScope;
lanes.stream().flatMap(lane -> lane.spaceStates().stream()).forEach(space -> uniqueSpaces.put(space.spaceId(),
productScope != null && matchedSpaceIds.contains(space.spaceId())
? withProductScope(space, productScope) : space));
buffers.stream().map(SpaceSnapshot::state).forEach(space -> uniqueSpaces.put(space.spaceId(), space));
Map<String, String> rawInboundTimes = new LinkedHashMap<>();
matches.stream().filter(match -> match.inboundTimeRaw() != null)
.forEach(match -> rawInboundTimes.putIfAbsent(match.spaceId(), match.inboundTimeRaw()));
lanes.stream().flatMap(lane -> lane.spaces().stream()).forEach(space -> space.inventory().stream()
.filter(item -> item.inboundTimeRaw() != null)
.forEach(item -> rawInboundTimes.putIfAbsent(space.spaceId(), item.inboundTimeRaw())));
OutboundPlan plan = outboundPlanner.plan(effectiveRequest,
new OutboundContext(List.copyOf(uniqueSpaces.values()), rawInboundTimes,
capacityMode(summary, policy),
repository.loadActiveDeepWorkflowFloors(warehouseId, request.requestId())));
String resultCode = switch (plan.status()) {
case READY -> "READY";
case WAITING -> "WAITING_" + plan.waiting().orElseThrow().code().name();
case ERROR -> "ERROR_" + plan.error().orElseThrow().code().name();
};
String reason = plan.assignment().map(value -> value.reason())
.or(() -> plan.waiting().map(value -> value.reason()))
.or(() -> plan.error().map(value -> value.message())).orElse(resultCode);
log(warehouseId, "OUTBOUND", null, resultCode, reason);
return plan;
}
public TidyPlan planTidy(String warehouseId, Instant now) {
RepositoryPolicy policy = repository.loadPolicy(warehouseId);
List<SpaceState> spaces = new ArrayList<>();
forEachLanePage(warehouseId, false,
snapshots -> snapshots.forEach(lane -> spaces.addAll(lane.spaceStates())));
List<TidyPendingOutbound> pending = repository.loadPendingOutboundJobs(warehouseId).stream()
.map(job -> new TidyPendingOutbound(job.projectNo(), job.productNo(), job.trayNo(),
job.sourceLaneCode())).toList();
TidyPlan plan = tidyPlanner.plan(new TidyRequest(spaces, pending, now,
policy.schedulingPolicy(), policy.inboundPolicy().commonProjectNos()));
String result = plan.candidates().isEmpty() ? "EMPTY" : "READY";
log(warehouseId, "TIDY", null, result,
plan.diagnostics().isEmpty() ? "candidates=" + plan.candidates().size()
: String.join("; ", plan.diagnostics()));
return plan;
}
public InboundReservationOutcome reserveInbound(String reservationId, String warehouseId, String projectNo,
InboundPlanningResult planned) {
var assignment = planned.plan().assignment().orElseThrow(
() -> new IllegalArgumentException("Only an accepted inbound plan can be reserved"));
ReservationResult result = repository.tryReserveInbound(new InboundReservation(reservationId, warehouseId,
assignment.laneCode(), assignment.spaceId(), projectNo, planned.laneVersion()));
String resultCode = result.status() == ReservationStatus.CONFLICT ? "REPLAN_REQUIRED" : result.status().name();
log(warehouseId, "INBOUND_RESERVATION", planned.laneVersion(), resultCode,
result.reasonCode() == null ? resultCode : result.reasonCode());
return new InboundReservationOutcome(resultCode, result);
}
public InboundReservationOutcome reserveInbound(InboundReservation reservation) {
ReservationResult result = repository.tryReserveInbound(reservation);
String resultCode = result.status() == ReservationStatus.CONFLICT ? "REPLAN_REQUIRED" : result.status().name();
log(reservation.warehouseId(), "INBOUND_RESERVATION", reservation.expectedVersion(), resultCode,
result.reasonCode() == null ? resultCode : result.reasonCode());
return new InboundReservationOutcome(resultCode, result);
}
public ReservationResult releaseReservation(String warehouseId, String reservationId) {
ReservationResult result = repository.releaseReservation(warehouseId, reservationId);
log(warehouseId, "RESERVATION_RELEASE", null, result.status().name(), result.reasonCode());
return result;
}
public DeepWorkflowMutationResult acquireDeepWorkflowLock(String warehouseId, String requestId, int floorNo) {
DeepWorkflowMutationResult result = repository.acquireDeepWorkflowLockResult(requestId, warehouseId, floorNo);
log(warehouseId, "DEEP_WORKFLOW_LOCK", null, result.changed() ? "ACQUIRED"
: result.reasonCode() == null ? "CONFLICT" : result.reasonCode(),
"request=" + requestId + ", floor=" + floorNo);
return result;
}
public DeepWorkflowMutationResult releaseDeepWorkflowLock(String warehouseId, String requestId, int floorNo) {
DeepWorkflowMutationResult result = repository.releaseDeepWorkflowLockResult(requestId, warehouseId, floorNo);
log(warehouseId, "DEEP_WORKFLOW_RELEASE", null, result.changed() ? "RELEASED"
: result.reasonCode() == null ? "NOT_FOUND" : result.reasonCode(),
"request=" + requestId + ", floor=" + floorNo);
return result;
}
private void forEachLanePage(String warehouseId, boolean candidatesOnly,
java.util.function.Consumer<List<LaneSnapshot>> consumer) {
String cursor = null;
do {
LanePage page = candidatesOnly
? repository.pageCandidateLanes(warehouseId, cursor, pageSize)
: repository.pageWarehouseLanes(warehouseId, cursor, pageSize);
List<String> laneCodes = page.lanes().stream().map(LaneCandidate::laneCode).toList();
if (!laneCodes.isEmpty()) consumer.accept(repository.loadLaneSnapshots(warehouseId, laneCodes));
cursor = page.nextLaneCode();
} while (cursor != null);
}
private void log(String warehouseId, String type, VersionToken version, String resultCode, String reason) {
repository.insertDecision(new DecisionLogRecord(UUID.randomUUID().toString(), warehouseId, type,
version, resultCode, reason));
}
private OutboundCapacityMode capacityMode(WarehouseSummary summary, RepositoryPolicy policy) {
int occupied = summary.occupiedCount();
if (occupied >= policy.schedulingPolicy().capacityFirstOccupied()) {
return OutboundCapacityMode.CAPACITY_FIRST;
}
if (occupied >= policy.schedulingPolicy().warningOccupied()) {
return OutboundCapacityMode.WARNING;
}
return OutboundCapacityMode.BALANCED;
}
private SpaceState withProductScope(SpaceState space, String productId) {
return new SpaceState(space.spaceId(), space.zoneCode(), space.laneCode(), space.depthOrder(),
space.laneCapacity(), space.floorNo(), space.aisleNo(), space.groupCoding(), space.rowCode(),
space.role(), space.status(), space.outboundTaskActive(), space.inventoryProjectConflict(),
space.trayNo(), space.projectNo(), productId, space.lotNo(), space.inboundAt(), space.outboundAt(),
space.reserved(), space.rowVersion(), space.physicalStockPresent());
}
private boolean nonBlank(String value) { return value != null && !value.isBlank(); }
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.algorithm.service;
import com.ruoyi.algorithm.repository.WarehouseSummary;
import java.util.List;
public record WarehouseDiagnostics(WarehouseSummary summary, List<String> issues) {
public WarehouseDiagnostics { issues = List.copyOf(issues); }
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.algorithm.tidy;
import java.util.List;
import java.util.Optional;
public record TidyCandidate(
TidyCandidateType type,
String projectNo,
List<TidyMove> moves,
Optional<String> releasesLaneCode,
String reason
) {
public TidyCandidate {
moves = List.copyOf(moves);
releasesLaneCode = releasesLaneCode == null ? Optional.empty() : releasesLaneCode;
}
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.algorithm.tidy;
public enum TidyCandidateType {
GUEST_RETURN,
DOWNGRADE_MERGE,
CONSOLIDATE_INCUBATION
}

View File

@@ -0,0 +1,4 @@
package com.ruoyi.algorithm.tidy;
public record TidyMove(String trayNo, String fromSpaceId, String toSpaceId) {
}

View File

@@ -0,0 +1,4 @@
package com.ruoyi.algorithm.tidy;
public record TidyPendingOutbound(String projectNo, String productId, String trayNo, String laneCode) {
}

View File

@@ -0,0 +1,10 @@
package com.ruoyi.algorithm.tidy;
import java.util.List;
public record TidyPlan(List<TidyCandidate> candidates, List<String> diagnostics) {
public TidyPlan {
candidates = List.copyOf(candidates);
diagnostics = List.copyOf(diagnostics);
}
}

View File

@@ -0,0 +1,358 @@
package com.ruoyi.algorithm.tidy;
import com.ruoyi.algorithm.model.SpaceRole;
import com.ruoyi.algorithm.model.SpaceState;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public final class TidyPlanner {
private static final Duration STAGNANT_AGE = Duration.ofHours(72);
public TidyPlan plan(TidyRequest request) {
Map<String, LaneView> lanes = laneViews(request.spaces());
Set<String> pendingLanes = pendingLaneCodes(request);
Set<String> usedSources = new HashSet<>();
Set<String> usedTargets = new HashSet<>();
List<TidyCandidate> candidates = new ArrayList<>();
List<String> diagnostics = new ArrayList<>();
Map<String, Instant> newestInboundByProject = newestInboundByProject(request.spaces());
addGuestReturns(lanes, pendingLanes, usedSources, usedTargets, candidates);
if (request.now() == null) {
diagnostics.add("No current time supplied; DOWNGRADE_MERGE candidates were skipped");
} else {
addDowngrades(lanes, pendingLanes, request.now(), newestInboundByProject,
request.commonProjectNos(), usedSources, usedTargets, candidates);
}
addConsolidations(lanes, pendingLanes, request.commonProjectNos(),
usedSources, usedTargets, candidates);
candidates.sort(Comparator.comparingInt((TidyCandidate candidate) -> candidate.type().ordinal())
.thenComparingInt(candidate -> candidate.releasesLaneCode().isPresent() ? 0 : 1)
.thenComparingInt(candidate -> candidate.moves().size())
.thenComparing(TidyCandidate::projectNo, Comparator.nullsLast(String::compareTo)));
int maximum = Math.max(0, request.policy().maxTidyCandidates());
if (candidates.size() > maximum) {
diagnostics.add("Tidy candidates exceeded policy maximum; retained first " + maximum);
candidates = new ArrayList<>(candidates.subList(0, maximum));
}
return new TidyPlan(candidates, diagnostics);
}
private void addGuestReturns(Map<String, LaneView> lanes, Set<String> pendingLanes,
Set<String> usedSources, Set<String> usedTargets,
List<TidyCandidate> candidates) {
lanes.values().stream().sorted(Comparator.comparing(LaneView::laneCode)).forEach(source -> {
if (source.corrupt() || source.guestProject() == null || pendingLanes.contains(source.laneCode())
|| laneTouched(source, usedSources, usedTargets)) return;
List<SpaceState> guests = source.guestSpaces().stream()
.sorted(Comparator.comparingInt(SpaceState::groupCoding).reversed()
.thenComparing(SpaceState::spaceId))
.toList();
if (guests.stream().anyMatch(space -> usedSources.contains(space.spaceId()))) return;
LaneView target = lanes.values().stream()
.filter(lane -> !lane.laneCode().equals(source.laneCode()))
.filter(lane -> !lane.corrupt() && lane.guestProject() == null)
.filter(lane -> source.guestProject().equals(lane.residentProject()))
.filter(lane -> !pendingLanes.contains(lane.laneCode()))
.filter(lane -> !laneTouched(lane, usedSources, usedTargets))
.filter(lane -> availableTargets(lane, usedTargets).size() >= guests.size())
.min(Comparator.comparingInt((LaneView lane) -> Math.abs(lane.floorNo() - source.floorNo()))
.thenComparing(Comparator.comparingInt((LaneView lane) -> lane.occupied().size()).reversed())
.thenComparing(LaneView::laneCode))
.orElse(null);
if (target == null) {
target = fallbackGuestDestination(source, source.guestProject(), guests.size(), lanes,
pendingLanes, usedSources, usedTargets);
}
if (target == null) return;
List<SpaceState> targetSpaces = availableTargets(target, usedTargets).subList(0, guests.size());
List<TidyMove> moves = moves(guests, targetSpaces);
reserve(moves, usedSources, usedTargets);
candidates.add(new TidyCandidate(TidyCandidateType.GUEST_RETURN, source.guestProject(), moves,
Optional.empty(), "Return guest project stock to its own lane"));
});
}
private void addDowngrades(Map<String, LaneView> lanes, Set<String> pendingLanes, Instant now,
Map<String, Instant> newestInboundByProject, Set<String> commonProjectNos,
Set<String> usedSources, Set<String> usedTargets,
List<TidyCandidate> candidates) {
Instant threshold = now.minus(STAGNANT_AGE);
lanes.values().stream().sorted(Comparator.comparing(LaneView::laneCode)).forEach(source -> {
if (source.capacity() != 5 || source.corrupt() || source.guestProject() != null
|| source.residentProject() == null || source.occupied().isEmpty()
|| source.occupied().size() > 2 || pendingLanes.contains(source.laneCode())
|| commonProjectNos.contains(source.residentProject())
|| laneTouched(source, usedSources, usedTargets)) return;
Instant projectNewest = newestInboundByProject.get(source.residentProject());
if (projectNewest == null || projectNewest.isAfter(threshold)) return;
LaneView target = source.occupied().size() == 1 ? lanes.values().stream()
.filter(lane -> lane.capacity() == 1 && lane.occupied().isEmpty() && !lane.corrupt())
.filter(lane -> singlePoolTier(lane) < 2)
.filter(lane -> !pendingLanes.contains(lane.laneCode()))
.filter(lane -> !laneTouched(lane, usedSources, usedTargets))
.filter(lane -> availableTargets(lane, usedTargets).size() == 1)
.min(Comparator.comparingInt(this::singlePoolTier)
.thenComparingInt(lane -> Math.abs(lane.floorNo() - source.floorNo()))
.thenComparing(LaneView::laneCode))
.orElse(null) : null;
if (target == null) target = lanes.values().stream()
.filter(lane -> lane.capacity() == 2 && lane.occupied().isEmpty() && !lane.corrupt())
.filter(lane -> !pendingLanes.contains(lane.laneCode()))
.filter(lane -> !laneTouched(lane, usedSources, usedTargets))
.filter(lane -> availableTargets(lane, usedTargets).size() >= source.occupied().size())
.min(Comparator.comparingInt((LaneView lane) -> Math.abs(lane.floorNo() - source.floorNo()))
.thenComparing(LaneView::laneCode))
.orElse(null);
if (target == null) return;
List<SpaceState> ordered = source.occupied().stream()
.sorted(Comparator.comparingInt(SpaceState::groupCoding).reversed()
.thenComparing(SpaceState::spaceId))
.toList();
List<TidyMove> moves = moves(ordered,
availableTargets(target, usedTargets).subList(0, ordered.size()));
reserve(moves, usedSources, usedTargets);
candidates.add(new TidyCandidate(TidyCandidateType.DOWNGRADE_MERGE, source.residentProject(), moves,
Optional.of(source.laneCode()), "Move stagnant stock to a shallower lane and release the deep lane"));
});
}
private void addConsolidations(Map<String, LaneView> lanes, Set<String> pendingLanes,
Set<String> commonProjectNos,
Set<String> usedSources, Set<String> usedTargets,
List<TidyCandidate> candidates) {
Set<String> projects = new HashSet<>();
lanes.values().forEach(lane -> {
if (lane.capacity() == 5 && !lane.corrupt() && lane.guestProject() == null
&& lane.residentProject() != null && !lane.occupied().isEmpty()
&& lane.occupied().stream().noneMatch(space -> usedSources.contains(space.spaceId()))
&& lane.spaces().stream().noneMatch(space -> usedTargets.contains(space.spaceId()))
&& !commonProjectNos.contains(lane.residentProject())
&& !availableTargets(lane, usedTargets).isEmpty() && !pendingLanes.contains(lane.laneCode())) {
projects.add(lane.residentProject());
}
});
projects.stream().sorted().forEach(project -> {
if (commonProjectNos.contains(project)) return;
List<LaneView> deep = lanes.values().stream()
.filter(lane -> lane.capacity() == 5 && !lane.corrupt() && lane.guestProject() == null)
.filter(lane -> project.equals(lane.residentProject()))
.filter(lane -> lane.occupied().stream().noneMatch(space -> usedSources.contains(space.spaceId())))
.filter(lane -> lane.spaces().stream().noneMatch(space -> usedTargets.contains(space.spaceId())))
.filter(lane -> !pendingLanes.contains(lane.laneCode()))
.sorted(Comparator.comparingInt((LaneView lane) -> lane.occupied().size()).reversed()
.thenComparing(LaneView::laneCode))
.toList();
if (deep.isEmpty()) return;
List<SpaceState> shallow = lanes.values().stream()
.filter(lane -> lane.capacity() <= 2 && !pendingLanes.contains(lane.laneCode()))
.filter(lane -> !laneTouched(lane, usedSources, usedTargets))
.flatMap(lane -> accessibleInPhysicalOrder(lane).stream())
.filter(space -> project.equals(space.projectNo()))
.filter(space -> !usedSources.contains(space.spaceId()))
.sorted(Comparator.comparing(SpaceState::laneCode)
.thenComparing(Comparator.comparingInt(SpaceState::groupCoding).reversed())
.thenComparing(SpaceState::spaceId))
.toList();
if (shallow.isEmpty()) return;
List<TidyMove> moves = new ArrayList<>();
for (SpaceState source : shallow) {
SpaceState target = deep.stream().flatMap(lane -> availableTargets(lane, usedTargets).stream())
.findFirst().orElse(null);
if (target == null) break;
TidyMove move = new TidyMove(source.trayNo(), source.spaceId(), target.spaceId());
moves.add(move);
reserve(List.of(move), usedSources, usedTargets);
}
if (moves.isEmpty()) return;
Optional<String> released = lanes.values().stream()
.filter(lane -> lane.capacity() <= 2)
.filter(lane -> lane.occupied().stream().allMatch(space -> usedSources.contains(space.spaceId())))
.filter(lane -> lane.occupied().stream().anyMatch(space -> project.equals(space.projectNo())))
.map(LaneView::laneCode).sorted().findFirst();
candidates.add(new TidyCandidate(TidyCandidateType.CONSOLIDATE_INCUBATION, project, moves, released,
"Consolidate accessible shallow stock into a current deep project lane"));
});
}
private LaneView fallbackGuestDestination(LaneView source, String projectNo, int requiredSlots,
Map<String, LaneView> lanes, Set<String> pendingLanes,
Set<String> usedSources, Set<String> usedTargets) {
int projectPallets = (int) lanes.values().stream()
.flatMap(lane -> lane.occupied().stream())
.filter(space -> projectNo.equals(space.projectNo()))
.count();
int[] depthOrder = projectPallets >= 2 ? new int[]{5, 3, 2} : new int[]{2, 3};
for (int depth : depthOrder) {
LaneView empty = lanes.values().stream()
.filter(lane -> lane.capacity() == depth && lane.occupied().isEmpty() && !lane.corrupt())
.filter(lane -> !pendingLanes.contains(lane.laneCode()))
.filter(lane -> !laneTouched(lane, usedSources, usedTargets))
.filter(lane -> lane.spaces().stream().allMatch(this::ordinaryAvailableStorage))
.filter(lane -> availableTargets(lane, usedTargets).size() >= requiredSlots)
.min(Comparator.comparingInt((LaneView lane) -> Math.abs(lane.floorNo() - source.floorNo()))
.thenComparing(LaneView::laneCode))
.orElse(null);
if (empty != null) return empty;
}
if (requiredSlots != 1) return null;
return lanes.values().stream()
.filter(lane -> lane.capacity() == 1 && lane.occupied().isEmpty() && !lane.corrupt())
.filter(lane -> !pendingLanes.contains(lane.laneCode()))
.filter(lane -> !laneTouched(lane, usedSources, usedTargets))
.filter(lane -> lane.spaces().stream().allMatch(this::ordinaryAvailableStorage))
.min(Comparator.comparingInt(this::singlePoolTier)
.thenComparingInt(lane -> Math.abs(lane.floorNo() - source.floorNo()))
.thenComparing(LaneView::laneCode))
.orElse(null);
}
private boolean ordinaryAvailableStorage(SpaceState space) {
return space.role() == SpaceRole.STORAGE && space.availableEmpty();
}
private int singlePoolTier(LaneView lane) {
String zone = lane.spaces().get(0).zoneCode();
if ("FLEX".equals(zone)) return 0;
if ("CACHE".equals(zone)) return 1;
return 2;
}
private boolean laneTouched(LaneView lane, Set<String> usedSources, Set<String> usedTargets) {
return lane.spaces().stream().anyMatch(space -> usedSources.contains(space.spaceId())
|| usedTargets.contains(space.spaceId()));
}
private Map<String, Instant> newestInboundByProject(List<SpaceState> spaces) {
Map<String, Instant> result = new HashMap<>();
spaces.stream().filter(SpaceState::occupied)
.filter(space -> !blank(space.projectNo()) && space.inboundAt() != null)
.forEach(space -> result.merge(space.projectNo(), space.inboundAt(),
(left, right) -> left.isAfter(right) ? left : right));
return result;
}
private List<SpaceState> accessibleInPhysicalOrder(LaneView lane) {
if (lane.corrupt() || lane.occupied().isEmpty()) return List.of();
return lane.occupied().stream()
.sorted(Comparator.comparingInt(SpaceState::groupCoding).reversed()
.thenComparing(SpaceState::spaceId))
.toList();
}
private List<TidyMove> moves(List<SpaceState> sources, List<SpaceState> targets) {
List<TidyMove> result = new ArrayList<>();
for (int index = 0; index < sources.size(); index++) {
result.add(new TidyMove(sources.get(index).trayNo(), sources.get(index).spaceId(), targets.get(index).spaceId()));
}
return result;
}
private void reserve(List<TidyMove> moves, Set<String> usedSources, Set<String> usedTargets) {
moves.forEach(move -> {
usedSources.add(move.fromSpaceId());
usedTargets.add(move.toSpaceId());
});
}
private List<SpaceState> availableTargets(LaneView lane, Set<String> usedTargets) {
if (lane.corrupt()) return List.of();
return lane.spaces().stream()
.filter(space -> space.groupCoding() > lane.occupied().size())
.filter(space -> space.role() == SpaceRole.STORAGE && space.availableEmpty())
.filter(space -> !usedTargets.contains(space.spaceId()))
.sorted(Comparator.comparingInt(SpaceState::groupCoding).thenComparing(SpaceState::spaceId))
.toList();
}
private Set<String> pendingLaneCodes(TidyRequest request) {
Set<String> result = new HashSet<>();
request.spaces().stream().filter(SpaceState::outboundTaskActive)
.map(SpaceState::laneCode).forEach(result::add);
for (TidyPendingOutbound pending : request.pendingOutbounds()) {
if (!blank(pending.laneCode())) result.add(pending.laneCode());
request.spaces().stream().filter(SpaceState::occupied).filter(space -> pendingMatches(space, pending))
.map(SpaceState::laneCode).forEach(result::add);
}
return result;
}
private boolean pendingMatches(SpaceState space, TidyPendingOutbound pending) {
if (!blank(pending.trayNo())) return pending.trayNo().equals(space.trayNo());
if (blank(pending.projectNo())) return false;
return pending.projectNo().equals(space.projectNo())
&& (blank(pending.productId()) || pending.productId().equals(space.productId()));
}
private Map<String, LaneView> laneViews(List<SpaceState> spaces) {
Map<String, List<SpaceState>> grouped = new LinkedHashMap<>();
spaces.forEach(space -> grouped.computeIfAbsent(space.laneCode(), ignored -> new ArrayList<>()).add(space));
Map<String, LaneView> result = new HashMap<>();
grouped.forEach((laneCode, members) -> result.put(laneCode, analyze(laneCode, members)));
return result;
}
private LaneView analyze(String laneCode, List<SpaceState> members) {
List<SpaceState> ordered = members.stream()
.sorted(Comparator.comparingInt(SpaceState::groupCoding).thenComparing(SpaceState::spaceId))
.toList();
int capacity = ordered.stream().mapToInt(SpaceState::laneCapacity).max().orElse(0);
List<SpaceState> occupied = ordered.stream().filter(SpaceState::occupied).toList();
boolean corrupt = ordered.size() != capacity;
boolean emptySeen = false;
List<String> segments = new ArrayList<>();
String last = null;
for (SpaceState space : ordered) {
if (!space.occupied()) {
emptySeen = true;
continue;
}
if (emptySeen || space.inventoryProjectConflict() || !space.outboundSelectable()) corrupt = true;
String projectNo = space.projectNo();
if (blank(projectNo)) {
corrupt = true;
continue;
}
if (!projectNo.equals(last)) {
segments.add(projectNo);
last = projectNo;
}
}
if (segments.size() > 2) corrupt = true;
String resident = segments.isEmpty() ? null : segments.get(0);
String guest = segments.size() < 2 ? null : segments.get(1);
String finalGuest = guest;
List<SpaceState> guests = guest == null ? List.of()
: occupied.stream().filter(space -> finalGuest.equals(space.projectNo())).toList();
int floor = ordered.isEmpty() ? 0 : ordered.get(0).floorNo();
return new LaneView(laneCode, capacity, floor, ordered, occupied, corrupt, resident, guest, guests);
}
private boolean blank(String value) {
return value == null || value.isBlank();
}
private record LaneView(
String laneCode,
int capacity,
int floorNo,
List<SpaceState> spaces,
List<SpaceState> occupied,
boolean corrupt,
String residentProject,
String guestProject,
List<SpaceState> guestSpaces
) {
}
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.algorithm.tidy;
import com.ruoyi.algorithm.model.SpaceState;
import com.ruoyi.algorithm.policy.SchedulingPolicy;
import java.time.Instant;
import java.util.List;
import java.util.Set;
public record TidyRequest(
List<SpaceState> spaces,
List<TidyPendingOutbound> pendingOutbounds,
Instant now,
SchedulingPolicy policy,
Set<String> commonProjectNos
) {
public TidyRequest {
spaces = List.copyOf(spaces);
pendingOutbounds = pendingOutbounds == null ? List.of() : List.copyOf(pendingOutbounds);
policy = policy == null ? SchedulingPolicy.ft546Defaults() : policy;
commonProjectNos = commonProjectNos == null ? Set.of("COMMON") : Set.copyOf(commonProjectNos);
}
public TidyRequest(List<SpaceState> spaces, List<TidyPendingOutbound> pendingOutbounds,
Instant now, SchedulingPolicy policy) {
this(spaces, pendingOutbounds, now, policy, Set.of("COMMON"));
}
}

View File

@@ -0,0 +1,224 @@
/* Read-only validation queries for a SQL Server 2016 installation. */
SET NOCOUNT ON;
SELECT CAST(SERVERPROPERTY('ProductVersion') AS varchar(30)) AS product_version,
DB_NAME() AS database_name;
SELECT o.type_desc, COUNT(*) AS object_count
FROM sys.objects o
JOIN sys.schemas s ON s.schema_id = o.schema_id
WHERE s.name = N'alg' AND o.type IN ('U', 'V', 'P')
GROUP BY o.type_desc
ORDER BY o.type_desc;
SELECT installed.object_kind, installed.object_name
FROM (
SELECT o.type_desc AS object_kind, o.name AS object_name
FROM sys.objects o
JOIN sys.schemas s ON s.schema_id = o.schema_id
WHERE s.name = N'alg' AND o.type IN ('U', 'V', 'P')
UNION ALL
SELECT 'INDEX' AS object_kind, i.name AS object_name
FROM sys.indexes i
JOIN sys.objects o ON o.object_id = i.object_id
JOIN sys.schemas s ON s.schema_id = o.schema_id
WHERE s.name = N'alg' AND i.name IS NOT NULL
) installed
ORDER BY installed.object_kind, installed.object_name;
SELECT warehouse_id,
COUNT_BIG(*) AS excluded_from_algorithm_topology_count,
SUM(CASE WHEN group_coding_no IS NULL OR LTRIM(RTRIM(group_coding_no)) = ''
THEN 1 ELSE 0 END) AS missing_lane_code_count,
SUM(CASE WHEN TRY_CONVERT(int, group_coding) IS NULL
THEN 1 ELSE 0 END) AS invalid_depth_count
FROM dbo.bas_cargo_space
WHERE group_coding_no IS NULL OR LTRIM(RTRIM(group_coding_no)) = ''
OR TRY_CONVERT(int, group_coding) IS NULL
GROUP BY warehouse_id
ORDER BY warehouse_id;
SELECT warehouse_id,
COUNT(*) AS space_state_count,
SUM(CASE WHEN space_status = 2 THEN 1 ELSE 0 END) AS full_space_count,
SUM(CASE WHEN inbound_available = 1 THEN 1 ELSE 0 END) AS inbound_available_count,
SUM(CASE WHEN pending_outbound = 1 THEN 1 ELSE 0 END) AS pending_outbound_space_count,
SUM(CASE WHEN has_gap = 1 THEN 1 ELSE 0 END) AS gap_space_count,
SUM(CASE WHEN tray_conflict = 1 OR project_conflict = 1 OR product_conflict = 1 THEN 1 ELSE 0 END) AS conflict_space_count
FROM alg.v_space_state
GROUP BY warehouse_id
ORDER BY warehouse_id;
SELECT warehouse_id, COUNT(*) AS topology_count
FROM alg.space_topology
GROUP BY warehouse_id
ORDER BY warehouse_id;
SELECT warehouse_id,
COUNT(*) AS inventory_detail_snapshot_count,
COUNT(DISTINCT space_id) AS inventory_space_snapshot_count,
MAX(synchronized_at) AS last_inventory_synchronized_at
FROM alg.inventory_item_snapshot
GROUP BY warehouse_id
ORDER BY warehouse_id;
;WITH warehouse_floors AS (
SELECT DISTINCT warehouse_id, the_floor AS floor_no
FROM dbo.bas_cargo_space
), role_counts AS (
SELECT cargo.warehouse_id, cargo.the_floor AS floor_no,
SUM(CASE WHEN role.role = 'RELOCATION_BUFFER' THEN 1 ELSE 0 END) AS relocation_buffer_count,
SUM(CASE WHEN role.role = 'OUTBOUND_BUFFER' THEN 1 ELSE 0 END) AS outbound_buffer_count,
SUM(CASE WHEN role.role = 'PROTECTED_CORE' THEN 1 ELSE 0 END) AS protected_core_count
FROM dbo.bas_cargo_space cargo
LEFT JOIN alg.space_role role ON role.space_id = cargo.cargo_space_id
GROUP BY cargo.warehouse_id, cargo.the_floor
)
SELECT floors.warehouse_id, floors.floor_no,
ISNULL(counts.relocation_buffer_count, 0) AS relocation_buffer_count,
CAST(4 AS int) AS relocation_buffer_target,
CASE WHEN ISNULL(counts.relocation_buffer_count, 0) >= 4 THEN 0
ELSE 4 - ISNULL(counts.relocation_buffer_count, 0) END AS relocation_buffer_shortage,
ISNULL(counts.outbound_buffer_count, 0) AS outbound_buffer_count,
ISNULL(counts.protected_core_count, 0) AS protected_core_count
FROM warehouse_floors floors
LEFT JOIN role_counts counts ON counts.warehouse_id = floors.warehouse_id
AND counts.floor_no = floors.floor_no
ORDER BY floors.warehouse_id, floors.floor_no;
SELECT cargo.warehouse_id, cargo.the_floor AS floor_no, role.space_id, role.role, role.updated_at
FROM alg.space_role role
JOIN dbo.bas_cargo_space cargo ON cargo.cargo_space_id = role.space_id
WHERE role.role IN ('RELOCATION_BUFFER', 'OUTBOUND_BUFFER', 'PROTECTED_CORE')
ORDER BY cargo.warehouse_id, cargo.the_floor, role.role, role.space_id;
SELECT warehouse_id, COUNT(*) AS lane_state_count,
SUM(CASE WHEN pending_outbound = 1 THEN 1 ELSE 0 END) AS pending_lane_count,
SUM(CASE WHEN has_gap = 1 THEN 1 ELSE 0 END) AS gap_lane_count
FROM alg.lane_state
GROUP BY warehouse_id
ORDER BY warehouse_id;
SELECT 'bas_cargo_space' AS table_name, COUNT_BIG(*) AS row_count,
CHECKSUM_AGG(BINARY_CHECKSUM(cargo_space_id, warehouse_id, group_coding_no, group_coding, status, tray_code)) AS row_checksum
FROM dbo.bas_cargo_space
UNION ALL
SELECT 'bas_stock', COUNT_BIG(*),
CHECKSUM_AGG(BINARY_CHECKSUM(stock_id, cargo_space_id, tray_code, spare4, product_id, lot_no, in_date))
FROM dbo.bas_stock
UNION ALL
SELECT 'mydo_job', COUNT_BIG(*),
CHECKSUM_AGG(BINARY_CHECKSUM(job_id, status, task_no, tray_code, cargo_space_id, target_cargo_space_id, in_or_out, warehouse_id))
FROM dbo.mydo_job;
;WITH pallet_details AS (
SELECT cargo_space_id,
MIN(NULLIF(LTRIM(RTRIM(tray_code)), '')) AS tray_code,
MIN(NULLIF(LTRIM(RTRIM(spare4)), '')) AS project_no,
MIN(NULLIF(LTRIM(RTRIM(product_id)), '')) AS first_product_no,
MAX(NULLIF(LTRIM(RTRIM(product_id)), '')) AS last_product_no,
COUNT(DISTINCT NULLIF(LTRIM(RTRIM(product_id)), '')) AS product_count
FROM dbo.bas_stock
GROUP BY cargo_space_id
)
SELECT TOP 5 state.space_id, details.tray_code, details.project_no,
details.first_product_no, details.last_product_no, details.product_count
FROM pallet_details details
JOIN alg.v_space_state state ON state.space_id = details.cargo_space_id
WHERE details.product_count > 1
AND state.space_status = '2'
AND state.stock_tray_count = 1
AND state.stock_project_count = 1
AND state.tray_conflict = 0
AND state.project_conflict = 0
AND state.pending_outbound = 0
AND state.reserved = 0
AND state.has_gap = 0
ORDER BY state.space_id;
SELECT space_id, warehouse_id, lane_code, depth, space_status, role,
tray_conflict, project_conflict, product_conflict, pending_outbound, reserved, has_gap,
inbound_available
FROM alg.v_space_state
WHERE tray_conflict = 1 OR project_conflict = 1 OR product_conflict = 1
OR pending_outbound = 1 OR has_gap = 1
ORDER BY warehouse_id, lane_code, depth;
SELECT reservation_id, warehouse_id, lane_code, space_id, project_no, status,
created_at, released_at
FROM alg.reservation
ORDER BY created_at DESC;
SELECT lock_id, request_id, warehouse_id, floor_no, status, created_at, released_at
FROM alg.deep_workflow_lock
ORDER BY created_at DESC;
SELECT warehouse_id, lane_code, topology_dirty, reason_mask, change_count,
enqueued_at, last_changed_at
FROM alg.dirty_lane_queue
ORDER BY last_changed_at, warehouse_id, lane_code;
SELECT workflow_id, request_id, stockout_detail_id, warehouse_id, source_stock_id,
selected_tray_code, source_space_id, lane_code, floor_no, exit_id,
status, current_step, total_steps, created_at, updated_at, completed_at
FROM alg.outbound_workflow
WHERE status <> 'COMPLETED'
ORDER BY created_at, workflow_id;
SELECT step.workflow_id, step.assignment_id, step.phase, step.source_space_id,
step.target_space_id, step.workflow_step, step.workflow_total_steps,
step.depends_on_assignment_id, step.stock_id, step.tray_code, step.status,
step.wms_job_id, step.created_at, step.started_at, step.at_exit_at, step.completed_at
FROM alg.outbound_workflow_step step
JOIN alg.outbound_workflow workflow ON workflow.workflow_id = step.workflow_id
WHERE workflow.status <> 'COMPLETED' OR step.status <> 'COMPLETED'
ORDER BY step.workflow_id, step.workflow_step;
SELECT mode.warehouse_id, mode.requested_enabled, mode.effective_state,
mode.status_reason, mode.mode_version, mode.updated_by, mode.updated_at
FROM alg.tidy_mode mode
ORDER BY mode.warehouse_id;
SELECT queue.warehouse_id, queue.lane_code, queue.dirty_version, queue.queue_status,
queue.reason_mask, queue.change_count, queue.last_dirty_at, queue.claimed_version,
queue.claimed_by, queue.claimed_at, queue.lease_expires_at, queue.last_error
FROM alg.tidy_lane_queue queue
ORDER BY queue.last_dirty_at, queue.warehouse_id, queue.lane_code;
SELECT workflow.workflow_id, workflow.warehouse_id, workflow.lane_code, workflow.origin,
workflow.candidate_type, workflow.source_space_id, workflow.target_space_id,
workflow.stock_id, workflow.tray_code, workflow.status, workflow.claimed_dirty_version,
workflow.vehicle_id, workflow.wcs_task_id, workflow.status_reason,
workflow.created_at, workflow.updated_at, workflow.dispatched_at, workflow.completed_at
FROM alg.tidy_workflow workflow
WHERE workflow.completed_at IS NULL
OR workflow.status IN ('PAUSED_RECOVERY', 'CANCELLED_RECONCILED')
ORDER BY workflow.created_at, workflow.workflow_id;
SELECT step.workflow_id, step.step_id, step.step_no, step.source_space_id,
step.target_space_id, step.stock_id, step.tray_code, step.status,
step.wms_job_id, step.wcs_task_id, step.expected_event_stage,
step.created_at, step.started_at, step.completed_at
FROM alg.tidy_step step
JOIN alg.tidy_workflow workflow ON workflow.workflow_id = step.workflow_id
WHERE workflow.completed_at IS NULL OR step.completed_at IS NULL
ORDER BY step.workflow_id, step.step_no;
SELECT lock.workflow_id, lock.warehouse_id, lock.resource_type, lock.resource_id,
lock.status, lock.locked_at, lock.released_at
FROM alg.tidy_space_lock lock
WHERE lock.status = 'ACTIVE'
ORDER BY lock.warehouse_id, lock.resource_type, lock.resource_id;
SELECT event.workflow_id, event.step_id, event.wcs_task_id, event.event_stage,
COUNT(*) AS callback_count, MIN(event.received_at) AS first_received_at,
MAX(event.received_at) AS last_received_at
FROM alg.tidy_callback_event event
GROUP BY event.workflow_id, event.step_id, event.wcs_task_id, event.event_stage
HAVING COUNT(*) > 1
ORDER BY event.workflow_id, event.step_id, event.event_stage;
SELECT workflow.origin, workflow.status, COUNT(*) AS workflow_count
FROM alg.tidy_workflow workflow
GROUP BY workflow.origin, workflow.status
ORDER BY workflow.origin, workflow.status;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
/* Removes only objects owned by schema alg. Run independently of install.sql. */
SET NOCOUNT ON;
IF OBJECT_ID(N'dbo.trg_alg_dirty_bas_stock', N'TR') IS NOT NULL DROP TRIGGER dbo.trg_alg_dirty_bas_stock;
IF OBJECT_ID(N'dbo.trg_alg_dirty_bas_cargo_space', N'TR') IS NOT NULL DROP TRIGGER dbo.trg_alg_dirty_bas_cargo_space;
IF OBJECT_ID(N'dbo.trg_alg_dirty_mydo_job', N'TR') IS NOT NULL DROP TRIGGER dbo.trg_alg_dirty_mydo_job;
IF OBJECT_ID(N'alg.trg_alg_dirty_space_role', N'TR') IS NOT NULL DROP TRIGGER alg.trg_alg_dirty_space_role;
IF OBJECT_ID(N'alg.trg_alg_dirty_project_policy', N'TR') IS NOT NULL DROP TRIGGER alg.trg_alg_dirty_project_policy;
IF OBJECT_ID(N'alg.trg_alg_dirty_warehouse_policy', N'TR') IS NOT NULL DROP TRIGGER alg.trg_alg_dirty_warehouse_policy;
GO
IF OBJECT_ID(N'alg.usp_refresh_dirty_lanes', N'P') IS NOT NULL DROP PROCEDURE alg.usp_refresh_dirty_lanes;
IF OBJECT_ID(N'alg.usp_mark_dirty_lane', N'P') IS NOT NULL DROP PROCEDURE alg.usp_mark_dirty_lane;
IF OBJECT_ID(N'alg.usp_claim_tidy_lane', N'P') IS NOT NULL DROP PROCEDURE alg.usp_claim_tidy_lane;
IF OBJECT_ID(N'alg.usp_release_tidy_lane', N'P') IS NOT NULL DROP PROCEDURE alg.usp_release_tidy_lane;
IF OBJECT_ID(N'alg.usp_complete_move_job', N'P') IS NOT NULL DROP PROCEDURE alg.usp_complete_move_job;
IF OBJECT_ID(N'alg.usp_sync_replay_sequences', N'P') IS NOT NULL DROP PROCEDURE alg.usp_sync_replay_sequences;
IF OBJECT_ID(N'alg.usp_release_reservation', N'P') IS NOT NULL DROP PROCEDURE alg.usp_release_reservation;
IF OBJECT_ID(N'alg.usp_try_reserve_inbound', N'P') IS NOT NULL DROP PROCEDURE alg.usp_try_reserve_inbound;
IF OBJECT_ID(N'alg.usp_apply_space_role', N'P') IS NOT NULL DROP PROCEDURE alg.usp_apply_space_role;
IF OBJECT_ID(N'alg.usp_release_deep_workflow_lock', N'P') IS NOT NULL DROP PROCEDURE alg.usp_release_deep_workflow_lock;
IF OBJECT_ID(N'alg.usp_acquire_deep_workflow_lock', N'P') IS NOT NULL DROP PROCEDURE alg.usp_acquire_deep_workflow_lock;
IF OBJECT_ID(N'alg.usp_rebuild_lane_state', N'P') IS NOT NULL DROP PROCEDURE alg.usp_rebuild_lane_state;
IF OBJECT_ID(N'alg.usp_compute_lane_semantic_fingerprint', N'P') IS NOT NULL DROP PROCEDURE alg.usp_compute_lane_semantic_fingerprint;
IF OBJECT_ID(N'alg.usp_sync_inventory_snapshot', N'P') IS NOT NULL DROP PROCEDURE alg.usp_sync_inventory_snapshot;
IF OBJECT_ID(N'alg.usp_rebuild_space_topology', N'P') IS NOT NULL DROP PROCEDURE alg.usp_rebuild_space_topology;
GO
IF OBJECT_ID(N'alg.v_space_state', N'V') IS NOT NULL DROP VIEW alg.v_space_state;
IF OBJECT_ID(N'alg.v_active_job', N'V') IS NOT NULL DROP VIEW alg.v_active_job;
IF OBJECT_ID(N'alg.v_inventory_item', N'V') IS NOT NULL DROP VIEW alg.v_inventory_item;
GO
IF OBJECT_ID(N'alg.outbound_workflow_space', N'U') IS NOT NULL DROP TABLE alg.outbound_workflow_space;
IF OBJECT_ID(N'alg.outbound_workflow_step', N'U') IS NOT NULL DROP TABLE alg.outbound_workflow_step;
IF OBJECT_ID(N'alg.outbound_workflow', N'U') IS NOT NULL DROP TABLE alg.outbound_workflow;
IF OBJECT_ID(N'alg.tidy_callback_event', N'U') IS NOT NULL DROP TABLE alg.tidy_callback_event;
IF OBJECT_ID(N'alg.tidy_space_lock', N'U') IS NOT NULL DROP TABLE alg.tidy_space_lock;
IF OBJECT_ID(N'alg.tidy_step', N'U') IS NOT NULL DROP TABLE alg.tidy_step;
IF OBJECT_ID(N'alg.tidy_workflow', N'U') IS NOT NULL DROP TABLE alg.tidy_workflow;
IF OBJECT_ID(N'alg.tidy_lane_queue', N'U') IS NOT NULL DROP TABLE alg.tidy_lane_queue;
IF OBJECT_ID(N'alg.tidy_mode', N'U') IS NOT NULL DROP TABLE alg.tidy_mode;
IF OBJECT_ID(N'alg.decision_log', N'U') IS NOT NULL DROP TABLE alg.decision_log;
IF OBJECT_ID(N'alg.deep_workflow_lock', N'U') IS NOT NULL DROP TABLE alg.deep_workflow_lock;
IF OBJECT_ID(N'alg.reservation', N'U') IS NOT NULL DROP TABLE alg.reservation;
IF OBJECT_ID(N'alg.dirty_lane_queue', N'U') IS NOT NULL DROP TABLE alg.dirty_lane_queue;
IF OBJECT_ID(N'alg.lane_state', N'U') IS NOT NULL DROP TABLE alg.lane_state;
IF OBJECT_ID(N'alg.space_inventory_summary', N'U') IS NOT NULL DROP TABLE alg.space_inventory_summary;
IF OBJECT_ID(N'alg.inventory_item_snapshot', N'U') IS NOT NULL DROP TABLE alg.inventory_item_snapshot;
IF OBJECT_ID(N'alg.space_topology', N'U') IS NOT NULL DROP TABLE alg.space_topology;
IF OBJECT_ID(N'alg.space_role', N'U') IS NOT NULL DROP TABLE alg.space_role;
IF OBJECT_ID(N'alg.project_policy', N'U') IS NOT NULL DROP TABLE alg.project_policy;
IF OBJECT_ID(N'alg.warehouse_policy', N'U') IS NOT NULL DROP TABLE alg.warehouse_policy;
GO
IF SCHEMA_ID(N'alg') IS NOT NULL EXEC(N'DROP SCHEMA alg');
GO