feat: prepare warehouse 01 floor 1 inbound pilot

This commit is contained in:
zgw
2026-09-16 14:12:12 +08:00
parent 2632a57d73
commit 1225cf7287
25 changed files with 1465 additions and 3 deletions

View File

@@ -0,0 +1,54 @@
package com.ruoyi.algorithm.pilot;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "fortucky.inbound-pilot")
public class Layer1InboundPilotProperties {
private boolean enabled;
private String warehouseId = "01";
private int floor = 1;
private long planningTimeoutMillis = 1500L;
private int snapshotMaxAgeSeconds = 60;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getWarehouseId() {
return warehouseId;
}
public void setWarehouseId(String warehouseId) {
this.warehouseId = warehouseId == null ? "" : warehouseId.trim();
}
public int getFloor() {
return floor;
}
public void setFloor(int floor) {
this.floor = floor;
}
public long getPlanningTimeoutMillis() {
return planningTimeoutMillis;
}
public void setPlanningTimeoutMillis(long planningTimeoutMillis) {
this.planningTimeoutMillis = planningTimeoutMillis;
}
public int getSnapshotMaxAgeSeconds() {
return snapshotMaxAgeSeconds;
}
public void setSnapshotMaxAgeSeconds(int snapshotMaxAgeSeconds) {
this.snapshotMaxAgeSeconds = snapshotMaxAgeSeconds;
}
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.algorithm.pilot;
public record Layer1InboundPilotResult(Status status, String finalSpaceId, String fallbackReason) {
public enum Status {
NOT_ELIGIBLE,
COMMITTED,
FALLBACK,
INDETERMINATE
}
public static Layer1InboundPilotResult notEligible() {
return new Layer1InboundPilotResult(Status.NOT_ELIGIBLE, null, null);
}
public static Layer1InboundPilotResult committed(String finalSpaceId) {
return new Layer1InboundPilotResult(Status.COMMITTED, finalSpaceId, null);
}
public static Layer1InboundPilotResult fallback(String reason) {
return new Layer1InboundPilotResult(Status.FALLBACK, null, reason);
}
public static Layer1InboundPilotResult indeterminate(String reason) {
return new Layer1InboundPilotResult(Status.INDETERMINATE, null, reason);
}
public boolean committed() {
return status == Status.COMMITTED;
}
}

View File

@@ -0,0 +1,303 @@
package com.ruoyi.algorithm.pilot;
import com.ruoyi.algorithm.shadow.InboundShadowObservation;
import com.ruoyi.algorithm.shadow.PublishedShadowSnapshot;
import com.ruoyi.algorithm.shadow.ShadowPlanningEngine;
import com.ruoyi.algorithm.shadow.ShadowSnapshotDescriptor;
import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper;
import com.ruoyi.api.domain.RequestEnter;
import com.ruoyi.job.mydojob.domain.MydoJob;
import com.ruoyi.job.mydojob.mapper.MydoJobMapper;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Component
public class Layer1InboundPilotService implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(Layer1InboundPilotService.class);
private static final String PUBLISHED = "PUBLISHED";
private static final String PILOT_WAREHOUSE_ID = "01";
private static final int PILOT_FLOOR = 1;
private static final int REASON_LIMIT = 3900;
private final Layer1InboundPilotProperties properties;
private final ShadowInputMapper shadowInputMapper;
private final ShadowPlanningEngine planningEngine;
private final MydoJobMapper jobMapper;
private final Clock clock;
private final ExecutorService planningExecutor;
@Autowired
public Layer1InboundPilotService(Layer1InboundPilotProperties properties,
ShadowInputMapper shadowInputMapper, ShadowPlanningEngine planningEngine,
MydoJobMapper jobMapper) {
this(properties, shadowInputMapper, planningEngine, jobMapper, Clock.systemUTC(), newPlanningExecutor());
}
Layer1InboundPilotService(Layer1InboundPilotProperties properties,
ShadowInputMapper shadowInputMapper, ShadowPlanningEngine planningEngine,
MydoJobMapper jobMapper, Clock clock, ExecutorService planningExecutor) {
this.properties = Objects.requireNonNull(properties, "properties");
this.shadowInputMapper = Objects.requireNonNull(shadowInputMapper, "shadowInputMapper");
this.planningEngine = Objects.requireNonNull(planningEngine, "planningEngine");
this.jobMapper = Objects.requireNonNull(jobMapper, "jobMapper");
this.clock = Objects.requireNonNull(clock, "clock");
this.planningExecutor = Objects.requireNonNull(planningExecutor, "planningExecutor");
}
public Layer1InboundPilotResult attempt(RequestEnter request, MydoJob job) {
if (!eligible(request, job)) {
return Layer1InboundPilotResult.notEligible();
}
long startedNanos = System.nanoTime();
long generation = 0L;
String proposedSpace = null;
Layer1InboundPilotResult result;
try {
Proposal proposal = planWithTimeout(request, job);
generation = proposal.generation();
proposedSpace = proposal.proposedSpaceId();
if (proposal.fallbackReason() != null) {
result = Layer1InboundPilotResult.fallback(proposal.fallbackReason());
} else {
result = mutate(request, job, proposal);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
result = Layer1InboundPilotResult.fallback("PLAN_INTERRUPTED");
} catch (TimeoutException timeout) {
result = Layer1InboundPilotResult.fallback("PLAN_TIMEOUT");
} catch (RejectedExecutionException busy) {
result = Layer1InboundPilotResult.fallback("PLAN_BUSY");
} catch (ExecutionException planningFailure) {
result = Layer1InboundPilotResult.fallback("PLAN_ERROR_" + simpleName(planningFailure.getCause()));
} catch (RuntimeException | Error unexpected) {
result = Layer1InboundPilotResult.fallback("PILOT_ERROR_" + simpleName(unexpected));
}
audit(request, job, generation, proposedSpace, result, elapsedMillis(startedNanos));
return result;
}
private Proposal planWithTimeout(RequestEnter request, MydoJob job)
throws InterruptedException, ExecutionException, TimeoutException {
long timeout = properties.getPlanningTimeoutMillis();
if (timeout < 1L || properties.getSnapshotMaxAgeSeconds() < 5
|| properties.getSnapshotMaxAgeSeconds() > 300) {
return Proposal.fallback("INVALID_PILOT_CONFIGURATION");
}
Future<Proposal> future = planningExecutor.submit(() -> plan(request, job));
try {
return future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException timeoutException) {
future.cancel(true);
throw timeoutException;
}
}
private Proposal plan(RequestEnter request, MydoJob job) {
String warehouseId = PILOT_WAREHOUSE_ID;
ShadowSnapshotDescriptor descriptor = shadowInputMapper.selectPublishedSnapshot(warehouseId);
String invalid = invalidSnapshot(descriptor, warehouseId);
if (invalid != null) {
return Proposal.fallback(invalid);
}
PublishedShadowSnapshot snapshot = new PublishedShadowSnapshot(
descriptor.getSnapshotGeneration(), descriptor.getWarehouseId(),
descriptor.getSynchronizedAt(), descriptor.getSourceSignature());
InboundShadowObservation observation = planningEngine.planInbound(snapshot,
request.getTaskNo(), job.getTrayCode(), PILOT_FLOOR);
if (!observation.observed() || isBlank(observation.proposedSpaceId())
|| !("READY".equals(observation.resultCode())
|| "READY_CONSERVATIVE".equals(observation.resultCode()))) {
return Proposal.fallback(observation.resultCode() == null
? "PLAN_NOT_READY" : "PLAN_" + observation.resultCode());
}
return new Proposal(observation.snapshotGeneration(), observation.proposedSpaceId(),
observation.reason(), null);
}
private String invalidSnapshot(ShadowSnapshotDescriptor descriptor, String warehouseId) {
if (descriptor == null || !PUBLISHED.equals(descriptor.getSnapshotStatus())
|| descriptor.getSnapshotGeneration() <= 0L
|| descriptor.getSynchronizedAt() == null || isBlank(descriptor.getSourceSignature())) {
return "SNAPSHOT_NOT_READY";
}
if (!warehouseEquivalent(descriptor.getWarehouseId(), warehouseId)) {
return "SNAPSHOT_WAREHOUSE_MISMATCH";
}
Duration age = Duration.between(descriptor.getSynchronizedAt(), clock.instant());
if (age.isNegative()) {
return "SNAPSHOT_TIME_INVALID";
}
if (age.compareTo(Duration.ofSeconds(properties.getSnapshotMaxAgeSeconds())) > 0) {
return "SNAPSHOT_STALE";
}
return null;
}
private Layer1InboundPilotResult mutate(RequestEnter request, MydoJob job, Proposal proposal) {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("v_task_no", request.getTaskNo());
arguments.put("v_tray_code", job.getTrayCode());
arguments.put("v_code", request.getDeviceCode());
arguments.put("v_space_id", proposal.proposedSpaceId());
arguments.put("v_warehouse_id", PILOT_WAREHOUSE_ID);
arguments.put("v_floor", PILOT_FLOOR);
arguments.put("v_snapshot_generation", proposal.generation());
arguments.put("v_snapshot_max_age_seconds", properties.getSnapshotMaxAgeSeconds());
try {
jobMapper.allocateLayer1InboundPilot(arguments);
int code = intValue(arguments.get("v_result"), -1);
String finalSpace = stringValue(arguments.get("v_final_space_id"));
if ((code == 10 || code == 11 || code == 12) && !isBlank(finalSpace)) {
return Layer1InboundPilotResult.committed(finalSpace);
}
return Layer1InboundPilotResult.fallback("MUTATION_" + code + "_"
+ abbreviate(stringValue(arguments.get("v_resultmsg")), 160));
} catch (RuntimeException | Error mutationFailure) {
return resolveMutationFailure(request.getTaskNo(), mutationFailure);
}
}
private Layer1InboundPilotResult resolveMutationFailure(String taskNo, Throwable mutationFailure) {
try {
MydoJob current = jobMapper.selectInJobByTaskNo(taskNo);
if (current != null && !isBlank(current.getCargoSpaceId())) {
return Layer1InboundPilotResult.committed(current.getCargoSpaceId());
}
return Layer1InboundPilotResult.fallback("MUTATION_ROLLED_BACK_" + simpleName(mutationFailure));
} catch (RuntimeException | Error stateReadFailure) {
LOG.error("layer1_inbound_pilot_indeterminate task={} mutationError={} stateReadError={}",
taskNo, simpleName(mutationFailure), simpleName(stateReadFailure));
return Layer1InboundPilotResult.indeterminate("MUTATION_STATE_UNKNOWN");
}
}
private boolean eligible(RequestEnter request, MydoJob job) {
return properties.isEnabled() && request != null && job != null
&& properties.getFloor() == PILOT_FLOOR
&& warehouseEquivalent(properties.getWarehouseId(), PILOT_WAREHOUSE_ID)
&& Integer.toString(PILOT_FLOOR).equals(trim(request.getFloor()))
&& warehouseEquivalent(job.getWarehouseId(), PILOT_WAREHOUSE_ID)
&& !isBlank(request.getTaskNo()) && !isBlank(job.getTrayCode())
&& job.getCargoSpaceId() == null;
}
private void audit(RequestEnter request, MydoJob job, long generation, String proposedSpace,
Layer1InboundPilotResult result, long elapsedMillis) {
String reason = "task=" + safe(request.getTaskNo())
+ "; tray=" + safe(job.getTrayCode())
+ "; proposedSpace=" + safe(proposedSpace)
+ "; finalSpace=" + safe(result.finalSpaceId())
+ "; elapsedMs=" + elapsedMillis
+ "; outcome=" + result.status()
+ "; fallbackReason=" + safe(result.fallbackReason());
try {
shadowInputMapper.insertPilotAudit(UUID.randomUUID().toString(), PILOT_WAREHOUSE_ID,
generation, result.status().name(), abbreviate(reason, REASON_LIMIT));
} catch (RuntimeException | Error auditFailure) {
LOG.warn("layer1_inbound_pilot_audit_failed task={} error={}",
request.getTaskNo(), simpleName(auditFailure));
}
}
@PreDestroy
@Override
public void close() {
planningExecutor.shutdownNow();
}
private static ExecutorService newPlanningExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), runnable -> {
Thread thread = new Thread(runnable, "layer1-inbound-pilot-planner");
thread.setDaemon(true);
return thread;
}, new ThreadPoolExecutor.AbortPolicy());
}
private static boolean warehouseEquivalent(String left, String right) {
String normalizedLeft = normalizeWarehouse(left);
String normalizedRight = normalizeWarehouse(right);
return normalizedLeft != null && normalizedLeft.equals(normalizedRight);
}
private static String normalizeWarehouse(String value) {
String trimmed = trim(value);
if (trimmed == null || trimmed.isEmpty()) {
return null;
}
if (trimmed.chars().allMatch(Character::isDigit)) {
String normalized = trimmed.replaceFirst("^0+(?!$)", "");
return normalized.isEmpty() ? "0" : normalized;
}
return trimmed;
}
private static int intValue(Object value, int fallback) {
if (value instanceof Number number) {
return number.intValue();
}
try {
return Integer.parseInt(Objects.toString(value, ""));
} catch (NumberFormatException ignored) {
return fallback;
}
}
private static String stringValue(Object value) {
return value == null ? null : value.toString();
}
private static String simpleName(Throwable failure) {
return failure == null ? "UNKNOWN" : failure.getClass().getSimpleName();
}
private static long elapsedMillis(long startedNanos) {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
}
private static String safe(String value) {
return value == null ? "" : abbreviate(value.replace(';', '_'), 255);
}
private static String abbreviate(String value, int limit) {
if (value == null || value.length() <= limit) {
return value;
}
return value.substring(0, limit);
}
private static String trim(String value) {
return value == null ? null : value.trim();
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
private record Proposal(long generation, String proposedSpaceId, String reason, String fallbackReason) {
private static Proposal fallback(String reason) {
return new Proposal(0L, null, null, reason);
}
}
}

View File

@@ -82,7 +82,16 @@ public class ShadowPlanningEngine {
public InboundShadowObservation planInbound(PublishedShadowSnapshot snapshot,
String taskNo, String trayNo) {
return planWithStableSnapshot(snapshot,
() -> planInboundInsideTransaction(snapshot, taskNo, trayNo));
() -> planInboundInsideTransaction(snapshot, taskNo, trayNo, null));
}
public InboundShadowObservation planInbound(PublishedShadowSnapshot snapshot,
String taskNo, String trayNo, Integer targetFloor) {
if (targetFloor == null || targetFloor <= 0) {
throw new IllegalArgumentException("targetFloor must be positive");
}
return planWithStableSnapshot(snapshot,
() -> planInboundInsideTransaction(snapshot, taskNo, trayNo, targetFloor));
}
public OutboundShadowObservation planProjectOutbound(PublishedShadowSnapshot snapshot,
@@ -245,7 +254,7 @@ public class ShadowPlanningEngine {
}
private InboundShadowObservation planInboundInsideTransaction(PublishedShadowSnapshot snapshot,
String taskNo, String trayNo) {
String taskNo, String trayNo, Integer targetFloor) {
String warehouseId = snapshot.warehouseId();
ShadowInboundReference reference = inputMapper.selectInboundInput(
warehouseId, snapshot.generation(), taskNo, trayNo);
@@ -294,7 +303,7 @@ public class ShadowPlanningEngine {
InboundRequest request = new InboundRequest(reference.getRequestId(), reference.getTrayNo(),
reference.getProjectNo(), multipleProducts ? null : reference.getProductId(),
multipleProducts ? null : reference.getLotNo(),
null, null, null, Set.of(), null, Set.copyOf(eligibleSpaces));
null, null, targetFloor, Set.of(), null, Set.copyOf(eligibleSpaces));
InboundPlanningResult result = schedulingFacade.planInboundFromPublishedSnapshot(warehouseId, request);
InboundPlan plan = result.plan();
if (plan.assignment().isPresent()) {

View File

@@ -50,6 +50,12 @@ public interface ShadowInputMapper {
@Param("resultCode") String resultCode,
@Param("reason") String reason);
int insertPilotAudit(@Param("decisionId") String decisionId,
@Param("warehouseId") String warehouseId,
@Param("inputVersion") long inputVersion,
@Param("resultCode") String resultCode,
@Param("reason") String reason);
void upsertRuntimeMetric(@Param("warehouseId") String warehouseId,
@Param("metricName") String metricName,
@Param("bucketUpperMicros") long bucketUpperMicros,

View File

@@ -8,6 +8,8 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.ruoyi.algorithm.shadow.InboundShadowCompletion;
import com.ruoyi.algorithm.shadow.ShadowObservationGateway;
import com.ruoyi.algorithm.pilot.Layer1InboundPilotResult;
import com.ruoyi.algorithm.pilot.Layer1InboundPilotService;
import com.ruoyi.api.domain.*;
import com.ruoyi.api.service.IToErpService;
import com.ruoyi.api.service.IToWmsService;
@@ -94,6 +96,8 @@ public class ToWmsServiceImpl implements IToWmsService {
private MydoJobMapper mydoJobMapper;
@Autowired
private ShadowObservationGateway shadowObservationGateway;
@Autowired
private Layer1InboundPilotService layer1InboundPilotService;
@Override
@@ -411,6 +415,22 @@ public class ToWmsServiceImpl implements IToWmsService {
if(inJob.getCargoSpaceId() != null && !"".equals(inJob.getCargoSpaceId())){
return AjaxResult.success(inJob);
}else{
Layer1InboundPilotResult pilotResult;
try {
pilotResult = layer1InboundPilotService.attempt(requestEnter, inJob);
} catch (RuntimeException | Error pilotFailure) {
pilotResult = Layer1InboundPilotResult.fallback("PILOT_BOUNDARY_FAILURE");
}
if (pilotResult.committed()) {
MydoJob pilotJob = mydoJobMapper.selectInJobByTaskNo(taskNo);
if (pilotJob == null) {
return AjaxResult.error("一层入库试点已提交但任务重读失败,请重新扫码确认任务货位");
}
return AjaxResult.success(pilotJob);
}
if (pilotResult.status() == Layer1InboundPilotResult.Status.INDETERMINATE) {
return AjaxResult.error("一层入库试点写入状态待确认,请重新扫码确认任务货位");
}
//分配货位
HashMap<String, Object> map = new HashMap<>();

View File

@@ -88,6 +88,8 @@ public interface MydoJobMapper
void allotInJobSpace(HashMap<String, Object> map);
void allocateLayer1InboundPilot(HashMap<String, Object> map);
void ykCompleted(HashMap<String, Object> map);
void updateLed(HashMap<String, Object> map);

View File

@@ -197,3 +197,9 @@ fortucky:
warehouse-id: ${FORTUCKY_ALGORITHM_WAREHOUSE_ID:01}
database-writes-enabled: ${FORTUCKY_ALGORITHM_DATABASE_WRITES_ENABLED:true}
legacy-fallback-on-technical-error: ${FORTUCKY_ALGORITHM_LEGACY_FALLBACK_ON_TECHNICAL_ERROR:false}
inbound-pilot:
enabled: ${FORTUCKY_INBOUND_PILOT_ENABLED:false}
warehouse-id: ${FORTUCKY_INBOUND_PILOT_WAREHOUSE_ID:01}
floor: ${FORTUCKY_INBOUND_PILOT_FLOOR:1}
planning-timeout-millis: ${FORTUCKY_INBOUND_PILOT_PLANNING_TIMEOUT_MILLIS:1500}
snapshot-max-age-seconds: ${FORTUCKY_INBOUND_PILOT_SNAPSHOT_MAX_AGE_SECONDS:60}

View File

@@ -232,6 +232,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and convert(varchar(64), state.source_signature, 2) = #{sourceSignature}
</insert>
<insert id="insertPilotAudit">
insert into alg.decision_log (
decision_id, warehouse_id, decision_type, input_version, result_code, reason)
values (try_convert(uniqueidentifier, #{decisionId}), #{warehouseId}, 'INBOUND_PILOT',
convert(binary(8), #{inputVersion}), #{resultCode}, #{reason})
</insert>
<update id="upsertRuntimeMetric">
merge alg.shadow_runtime_metric with (holdlock) as target
using (select #{warehouseId} as warehouse_id,

View File

@@ -308,6 +308,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)}
</select>
<select id="allocateLayer1InboundPilot" parameterType="java.util.Map" statementType="CALLABLE" timeout="3">
{call proc_allot_layer1_inbound_pilot(
#{v_task_no,jdbcType=VARCHAR,mode=IN},
#{v_tray_code,jdbcType=VARCHAR,mode=IN},
#{v_code,jdbcType=VARCHAR,mode=IN},
#{v_space_id,jdbcType=VARCHAR,mode=IN},
#{v_warehouse_id,jdbcType=VARCHAR,mode=IN},
#{v_floor,jdbcType=INTEGER,mode=IN},
#{v_snapshot_generation,jdbcType=BIGINT,mode=IN},
#{v_snapshot_max_age_seconds,jdbcType=INTEGER,mode=IN},
#{v_result,mode=OUT,jdbcType=INTEGER},
#{v_resultmsg,mode=OUT,jdbcType=VARCHAR},
#{v_final_space_id,mode=OUT,jdbcType=VARCHAR}
)}
</select>
<select id="updateLed" parameterType="java.util.Map" statementType="CALLABLE">
{call proc_update_led(
#{v_code,jdbcType=VARCHAR,mode=IN},