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" } }