From 1fd45f4f6904c1aabc70ab87c92172c683a10948 Mon Sep 17 00:00:00 2001
From: zgw <13623675548@163.com>
Date: Fri, 7 Aug 2026 13:34:54 +0800
Subject: [PATCH] feat(javaTest): load source task and aisle front fields
---
.../history/HistoricalChannelFrontRecord.java | 22 ++++++++
.../history/HistoricalQueueDataset.java | 29 ++++++++++
.../shadow/history/HistoricalQueueLoader.java | 13 ++++-
.../shadow/history/HistoricalQueueRecord.java | 19 +++++++
.../simulation/ChannelFrontTopology.java | 56 +++++++++++++++++++
.../wcs/shadow/HistoricalQueueLoaderTest.java | 16 ++++++
.../simulation/ChannelFrontTopologyTest.java | 29 ++++++++++
.../shadow/history/channel-fronts.json | 19 +++++++
.../resources/shadow/history/manifest.json | 4 +-
9 files changed, 205 insertions(+), 2 deletions(-)
create mode 100644 javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalChannelFrontRecord.java
create mode 100644 javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopology.java
create mode 100644 javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopologyTest.java
create mode 100644 javaTest/src/test/resources/shadow/history/channel-fronts.json
diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalChannelFrontRecord.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalChannelFrontRecord.java
new file mode 100644
index 0000000..dcd691a
--- /dev/null
+++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalChannelFrontRecord.java
@@ -0,0 +1,22 @@
+package com.ft546.wcs.shadow.history;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * 保存从生产 bas_cargo_space 只读查询导出的货位分组和通道前端事实。
+ *
+ *
仿真只读取该离线快照,不在运行时访问生产数据库,也不通过站点编号或坐标猜测前端。
+ */
+public final class HistoricalChannelFrontRecord {
+ @JsonProperty("site_id") private String siteId;
+ @JsonProperty("group_coding_no") private String groupCodingNo;
+ @JsonProperty("group_coding") private String groupCoding;
+ @JsonProperty("front_site_id") private String frontSiteId;
+
+ public HistoricalChannelFrontRecord() { }
+
+ public String getSiteId() { return siteId; }
+ public String getGroupCodingNo() { return groupCodingNo; }
+ public String getGroupCoding() { return groupCoding; }
+ public String getFrontSiteId() { return frontSiteId; }
+}
diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java
index 24dae90..88410df 100644
--- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java
+++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java
@@ -26,6 +26,7 @@ public final class HistoricalQueueDataset {
private final int missingPathRows;
private final int missingIntervalRows;
private final int reversedIntervalRows;
+ private final List channelFrontRecords;
/**
* 创建 {@code HistoricalQueueDataset} 实例并初始化其内部状态。
@@ -59,6 +60,27 @@ public final class HistoricalQueueDataset {
String minFinishDate, String maxFinishDate,
int missingPathRows, int missingIntervalRows,
int reversedIntervalRows) {
+ this(records, mapSiteDetails, fullMapCells, fullMapRows, fullMapSha256,
+ manifestSampleRows, fullScanTotalRows, database, sourceTable, extractedAt,
+ minCreateDate, maxCreateDate, minFinishDate, maxFinishDate,
+ missingPathRows, missingIntervalRows, reversedIntervalRows,
+ Collections.emptyList());
+ }
+
+ /**
+ * 创建带生产通道前端快照的数据集,使业务归并和地图数据来自同一份可审计离线输入。
+ */
+ public HistoricalQueueDataset(List records,
+ Map mapSiteDetails,
+ List fullMapCells,
+ int fullMapRows, String fullMapSha256,
+ int manifestSampleRows, int fullScanTotalRows,
+ String database, String sourceTable, String extractedAt,
+ String minCreateDate, String maxCreateDate,
+ String minFinishDate, String maxFinishDate,
+ int missingPathRows, int missingIntervalRows,
+ int reversedIntervalRows,
+ List channelFrontRecords) {
this.records = Collections.unmodifiableList(new ArrayList(records));
this.mapSiteDetails = Collections.unmodifiableMap(
new LinkedHashMap(mapSiteDetails));
@@ -78,6 +100,8 @@ public final class HistoricalQueueDataset {
this.missingPathRows = missingPathRows;
this.missingIntervalRows = missingIntervalRows;
this.reversedIntervalRows = reversedIntervalRows;
+ this.channelFrontRecords = Collections.unmodifiableList(
+ new ArrayList(channelFrontRecords));
}
/**
@@ -209,4 +233,9 @@ public final class HistoricalQueueDataset {
* @return 本次处理产生的结果;调用方据此继续后续流程。
*/
public int getReversedIntervalRows() { return reversedIntervalRows; }
+
+ /** 返回由生产货位分组查询固化的通道前端事实。 */
+ public List getChannelFrontRecords() {
+ return channelFrontRecords;
+ }
}
diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java
index a3762b6..9e0e25f 100644
--- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java
+++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java
@@ -40,17 +40,27 @@ public final class HistoricalQueueLoader {
byte[] mapBytes = read(loader, basePath + "/map-sites.json");
byte[] summaryBytes = read(loader, basePath + "/full-scan-summary.json");
byte[] fullMapBytes = read(loader, basePath + "/full-map-grid.json");
+ byte[] channelFrontBytes = read(loader, basePath + "/channel-fronts.json");
verifySha256(manifest.path("sha256").path("queue-history-200.json").asText(), queueBytes);
verifySha256(manifest.path("sha256").path("map-sites.json").asText(), mapBytes);
verifySha256(manifest.path("sha256").path("full-scan-summary.json").asText(), summaryBytes);
String fullMapSha256 = manifest.path("sha256").path("full-map-grid.json").asText();
verifySha256(fullMapSha256, fullMapBytes);
+ verifySha256(manifest.path("sha256").path("channel-fronts.json").asText(),
+ channelFrontBytes);
CollectionType rowsType = MAPPER.getTypeFactory()
.constructCollectionType(List.class, HistoricalQueueRecord.class);
List rows = MAPPER.readValue(queueBytes, rowsType);
CollectionType cellsType = MAPPER.getTypeFactory()
.constructCollectionType(List.class, HistoricalMapCell.class);
List fullMapCells = MAPPER.readValue(fullMapBytes, cellsType);
+ CollectionType channelFrontType = MAPPER.getTypeFactory()
+ .constructCollectionType(List.class, HistoricalChannelFrontRecord.class);
+ List channelFrontRecords =
+ MAPPER.readValue(channelFrontBytes, channelFrontType);
+ if (manifest.path("channelFrontRows").asInt() != channelFrontRecords.size()) {
+ throw new IllegalArgumentException("channel front row count mismatch");
+ }
int fullMapRows = manifest.path("fullMapRows").asInt();
if (fullMapRows != fullMapCells.size()) {
throw new IllegalArgumentException("full map row count mismatch");
@@ -92,7 +102,8 @@ public final class HistoricalQueueLoader {
scanSummary.path("max_finish_date").asText(),
scanSummary.path("missing_path_rows").asInt(),
scanSummary.path("missing_interval_rows").asInt(),
- scanSummary.path("reversed_interval_rows").asInt());
+ scanSummary.path("reversed_interval_rows").asInt(),
+ channelFrontRecords);
} catch (IOException ex) {
throw new IllegalArgumentException("cannot load history snapshot", ex);
}
diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueRecord.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueRecord.java
index e8e62d3..37b9c14 100644
--- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueRecord.java
+++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueRecord.java
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public final class HistoricalQueueRecord {
+ @JsonProperty("snapshot_row") private String snapshotRow;
@JsonProperty("guid") private String guid;
@JsonProperty("task_no") private String taskNo;
@JsonProperty("task_type") private String taskType;
@@ -12,6 +13,8 @@ public final class HistoricalQueueRecord {
@JsonProperty("cargo") private String cargo;
@JsonProperty("issued") private String issued;
@JsonProperty("execution_status") private String executionStatus;
+ @JsonProperty("cargo_pick_site") private String cargoPickSite;
+ @JsonProperty("cargo_place_site") private String cargoPlaceSite;
@JsonProperty("start_site") private String startSite;
@JsonProperty("end_site") private String endSite;
@JsonProperty("the_floor") private String floor;
@@ -20,6 +23,9 @@ public final class HistoricalQueueRecord {
@JsonProperty("start_date") private String startDate;
@JsonProperty("finish_date") private String finishDate;
@JsonProperty("agv_id") private String agvId;
+ @JsonProperty("default02") private String default02;
+ @JsonProperty("default06") private String default06;
+ @JsonProperty("default08") private String default08;
/**
* 创建 {@code HistoricalQueueRecord} 实例并初始化其内部状态。
@@ -58,6 +64,9 @@ public final class HistoricalQueueRecord {
this.agvId = agvId;
}
+ /** 返回本行在数据库扫描快照中的稳定行号,用于识别重复扫描而不是重新创建业务任务。 */
+ public String getSnapshotRow() { return snapshotRow; }
+
/**
* 返回当前对象保存的“guid”信息。
*
@@ -107,6 +116,10 @@ public final class HistoricalQueueRecord {
* @return 本次处理产生的结果;调用方据此继续后续流程。
*/
public String getExecutionStatus() { return executionStatus; }
+ /** 返回生产队列保存的业务取货点;二段任务以该点而不是队列当前 start_site 为业务源点。 */
+ public String getCargoPickSite() { return cargoPickSite; }
+ /** 返回生产队列保存的业务放货点;二段任务以该点而不是阶段 end_site 为业务目标。 */
+ public String getCargoPlaceSite() { return cargoPlaceSite; }
/**
* 返回当前对象保存的“start site”信息。
*
@@ -163,4 +176,10 @@ public final class HistoricalQueueRecord {
* @return 本次处理产生的结果;调用方据此继续后续流程。
*/
public String getAgvId() { return agvId; }
+ /** 返回生产队列的业务/设备阶段编码,其中 1、2、7 分别代表入库、出库和移库。 */
+ public String getDefault02() { return default02; }
+ /** 返回生产多段任务保存的通道前端站点。 */
+ public String getDefault06() { return default06; }
+ /** 返回生产多段任务的首次/后续下发标记。 */
+ public String getDefault08() { return default08; }
}
diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopology.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopology.java
new file mode 100644
index 0000000..a9c73f9
--- /dev/null
+++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopology.java
@@ -0,0 +1,56 @@
+package com.ft546.wcs.shadow.history.simulation;
+
+import com.ft546.wcs.shadow.history.HistoricalChannelFrontRecord;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/** 根据生产货位分组快照提供权威的通道前端查询,不向普通地图增加虚构边。 */
+public final class ChannelFrontTopology {
+ private final Map frontBySite;
+
+ private ChannelFrontTopology(Map frontBySite) {
+ this.frontBySite = Collections.unmodifiableMap(
+ new LinkedHashMap(frontBySite));
+ }
+
+ /**
+ * 将 SQL 快照转换为站点到前端的唯一映射;同一站点出现冲突时拒绝启动仿真。
+ */
+ public static ChannelFrontTopology from(List records) {
+ if (records == null) throw new IllegalArgumentException("channel front records are required");
+ Map values = new LinkedHashMap();
+ for (HistoricalChannelFrontRecord record : records) {
+ String site = required(record.getSiteId(), "site id");
+ String front = required(record.getFrontSiteId(), "front site id");
+ String previous = values.put(site, front);
+ if (previous != null && !previous.equals(front)) {
+ throw new IllegalArgumentException("conflicting channel front for " + site);
+ }
+ }
+ return new ChannelFrontTopology(values);
+ }
+
+ /** 返回货位所属通道的权威前端;非货位输送点返回空。 */
+ public Optional frontFor(String siteId) {
+ return Optional.ofNullable(frontBySite.get(siteId));
+ }
+
+ /** 判断业务货位与通道前端是否不同,从而是否需要生成显式前端移动阶段。 */
+ public boolean requiresFrontMovement(String siteId) {
+ Optional front = frontFor(siteId);
+ return front.isPresent() && !front.get().equals(siteId);
+ }
+
+ public Map asMap() { return frontBySite; }
+
+ private static String required(String value, String name) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new IllegalArgumentException(name + " is required");
+ }
+ return value.trim();
+ }
+}
diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java
index b818420..4599a1f 100644
--- a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java
+++ b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java
@@ -59,6 +59,22 @@ class HistoricalQueueLoaderTest {
assertEquals(5193, data.getMissingPathRows());
}
+ /**
+ * 防止生产队列阶段字段在 JSON 反序列化时被静默丢弃;业务任务归并必须使用这些原始事实。
+ */
+ @Test
+ void loadsSourceQueueStageFieldsAndChannelFrontSnapshot() {
+ HistoricalQueueDataset data = new HistoricalQueueLoader().load(
+ getClass().getClassLoader(), "shadow/history");
+
+ assertEquals("1393", data.getRecords().get(0).getSnapshotRow());
+ assertEquals("050101", data.getRecords().get(0).getCargoPickSite());
+ assertEquals("030401", data.getRecords().get(0).getCargoPlaceSite());
+ assertEquals("8", data.getRecords().get(0).getDefault02());
+ assertEquals("", data.getRecords().get(0).getDefault08());
+ assertEquals(17, data.getChannelFrontRecords().size());
+ }
+
/**
* 验证“rejects hash mismatch”场景,确保相关业务规则和断言成立。
*
diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopologyTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopologyTest.java
new file mode 100644
index 0000000..2f00c9a
--- /dev/null
+++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/ChannelFrontTopologyTest.java
@@ -0,0 +1,29 @@
+package com.ft546.wcs.shadow.history.simulation;
+
+import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
+import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ChannelFrontTopologyTest {
+ /**
+ * 防止把站点编号或坐标误当成通道前端;结果必须来自固化的 bas_cargo_space 分组快照。
+ */
+ @Test
+ void resolvesTheSameFrontAsTheProductionGroupCodingQuery() {
+ HistoricalQueueDataset data = new HistoricalQueueLoader().load(
+ getClass().getClassLoader(), "shadow/history");
+ ChannelFrontTopology topology = ChannelFrontTopology.from(
+ data.getChannelFrontRecords());
+
+ assertEquals("050301", topology.frontFor("030301").get());
+ assertEquals("050401", topology.frontFor("040401").get());
+ assertEquals("050701", topology.frontFor("010701").get());
+ assertTrue(topology.requiresFrontMovement("030301"));
+ assertFalse(topology.requiresFrontMovement("050203"));
+ assertFalse(topology.frontFor("070101").isPresent());
+ }
+}
diff --git a/javaTest/src/test/resources/shadow/history/channel-fronts.json b/javaTest/src/test/resources/shadow/history/channel-fronts.json
new file mode 100644
index 0000000..88b589e
--- /dev/null
+++ b/javaTest/src/test/resources/shadow/history/channel-fronts.json
@@ -0,0 +1,19 @@
+[
+ {"site_id":"010701","group_coding_no":"A0701","group_coding":"1","front_site_id":"050701"},
+ {"site_id":"030301","group_coding_no":"A0301","group_coding":"1","front_site_id":"050301"},
+ {"site_id":"030401","group_coding_no":"A0401","group_coding":"1","front_site_id":"050401"},
+ {"site_id":"030501","group_coding_no":"A0501","group_coding":"1","front_site_id":"050501"},
+ {"site_id":"030601","group_coding_no":"A0601","group_coding":"1","front_site_id":"050601"},
+ {"site_id":"040301","group_coding_no":"A0301","group_coding":"2","front_site_id":"050301"},
+ {"site_id":"040401","group_coding_no":"A0401","group_coding":"2","front_site_id":"050401"},
+ {"site_id":"050203","group_coding_no":"A0203","group_coding":"5","front_site_id":"050203"},
+ {"site_id":"081301","group_coding_no":"C1301","group_coding":"1","front_site_id":"071301"},
+ {"site_id":"090901","group_coding_no":"D0901","group_coding":"1","front_site_id":"090901"},
+ {"site_id":"091101","group_coding_no":"D1101","group_coding":"1","front_site_id":"091101"},
+ {"site_id":"110301","group_coding_no":"B0301","group_coding":"1","front_site_id":"110301"},
+ {"site_id":"110302","group_coding_no":"B0302","group_coding":"1","front_site_id":"110302"},
+ {"site_id":"110402","group_coding_no":"B0402","group_coding":"1","front_site_id":"110402"},
+ {"site_id":"110501","group_coding_no":"B0501","group_coding":"1","front_site_id":"110501"},
+ {"site_id":"110601","group_coding_no":"B0601","group_coding":"1","front_site_id":"110601"},
+ {"site_id":"110801","group_coding_no":"B0801","group_coding":"1","front_site_id":"110801"}
+]
diff --git a/javaTest/src/test/resources/shadow/history/manifest.json b/javaTest/src/test/resources/shadow/history/manifest.json
index e8d32a5..cc8ba48 100644
--- a/javaTest/src/test/resources/shadow/history/manifest.json
+++ b/javaTest/src/test/resources/shadow/history/manifest.json
@@ -6,11 +6,13 @@
"sampleRows": 240,
"mapRows": 1129,
"fullMapRows": 2552,
+ "channelFrontRows": 17,
"queryId": "wcs-real-queue-history-replay-v1",
"sha256": {
"queue-history-200.json": "8f06d18354c8c199906e75d4243f6d993853d48d1d956fafee36e9f828ab5a94",
"map-sites.json": "8a16f0c797f6f5e952cd07ce5a50af3a5069c6c102604c20a4e816d2aa88d08d",
"full-scan-summary.json": "9186836b0c162ea302085fab6ccaf87ffa1733a869f42d2b8bf489a910e36d63",
- "full-map-grid.json": "f99ce691fa1935fa2aaf4db595143f7757e9baf7b4852f2bace24248c70da7f5"
+ "full-map-grid.json": "f99ce691fa1935fa2aaf4db595143f7757e9baf7b4852f2bace24248c70da7f5",
+ "channel-fronts.json": "a4c6a86d815caedc05eda644d62b857e666516a175d3a5dcde851f7eb5178e77"
}
}