feat(wcs): publish immutable warehouse configuration

This commit is contained in:
zgw
2026-08-10 15:55:59 +08:00
parent 81d96a671c
commit 67e47bbdc5
15 changed files with 916 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
package com.ruoyi.wcs.application.configuration;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
import com.ruoyi.wcs.domain.topology.TopologyEdge;
import com.ruoyi.wcs.domain.topology.TopologyNode;
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
import com.ruoyi.wcs.port.out.ConfigurationRepository;
import com.ruoyi.wcs.port.out.TopologyDefinitionProvider;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ConfigurationApplicationServiceTest {
@Test
void publishesOneImmutableConfigurationVersionAtomically() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
ConfigurationApplicationService service = service(repository,
definition(7L));
ConfigurationPublicationResult result = service.publish(
new PublishConfigurationCommand("FT546", 7L));
assertEquals(7L, result.getPublishedVersion());
assertEquals(1, repository.publishCount);
assertEquals(0, repository.partialWriteCount);
assertEquals(7L, service.readActive("FT546").getTopologyVersion());
}
@Test
void readsOneInternallyConsistentPublishedVersion() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
ConfigurationApplicationService service = service(repository,
definition(7L));
service.publish(new PublishConfigurationCommand("FT546", 7L));
PublishedConfigurationView view = service.read("FT546", 7L);
assertEquals(7L, view.getTopologyVersion());
assertEquals(7L, view.getDeviceConfigurationVersion());
assertEquals(7L, view.getWorkflowConfigurationVersion());
}
@Test
void refusesToReplaceVersionReferencedByAnActiveOrder() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
Map<Long, TopologyDefinition> definitions = new LinkedHashMap<Long, TopologyDefinition>();
definitions.put(7L, definition(7L));
definitions.put(8L, definition(8L));
ConfigurationApplicationService service = service(repository,
(warehouseCode, version) -> definitions.get(version));
service.publish(new PublishConfigurationCommand("FT546", 7L));
repository.activeReferences = true;
DomainRuleViolation error = assertThrows(DomainRuleViolation.class,
() -> service.publish(new PublishConfigurationCommand("FT546", 8L)));
assertEquals("active orders reference configuration version 7", error.getMessage());
assertEquals(1, repository.publishCount);
assertEquals(7L, service.readActive("FT546").getTopologyVersion());
}
@Test
void validationNeverWritesRepositoryState() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
ConfigurationApplicationService service = service(repository,
definition(7L));
service.validate(new PublishConfigurationCommand("FT546", 7L));
assertEquals(0, repository.publishCount);
}
private static ConfigurationApplicationService service(
InMemoryConfigurationRepository repository,
TopologyDefinition definition) {
return service(repository, (warehouseCode, version) -> definition);
}
private static ConfigurationApplicationService service(
InMemoryConfigurationRepository repository,
TopologyDefinitionProvider provider) {
return new ConfigurationApplicationService(provider,
new TopologyCompiler(), repository);
}
private static TopologyDefinition definition(long version) {
return new TopologyDefinition("FT546", version, true,
Arrays.asList(
new TopologyNode("A", "F1", 0, 0, 1, false, false),
new TopologyNode("B", "F1", 1, 0, 1, false, false)),
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.<ResourceDefinition>emptyList(),
Collections.<String>emptyList(), Collections.<String>emptyList());
}
private static final class InMemoryConfigurationRepository
implements ConfigurationRepository {
private final Map<Long, PublishedConfigurationView> versions =
new LinkedHashMap<Long, PublishedConfigurationView>();
private PublishedConfigurationView active;
private boolean activeReferences;
private int publishCount;
private int partialWriteCount;
@Override
public void publish(PublishConfigurationCommand command,
EffectiveTopology topology) {
PublishedConfigurationView view = PublishedConfigurationView.from(topology);
versions.put(command.getVersion(), view);
active = view;
publishCount++;
}
@Override
public PublishedConfigurationView find(String warehouseCode, long version) {
return versions.get(version);
}
@Override
public PublishedConfigurationView findActive(String warehouseCode) {
return active;
}
@Override
public boolean hasActiveOrderReferences(String warehouseCode, long version) {
return activeReferences && active != null
&& active.getTopologyVersion() == version;
}
}
}

View File

@@ -0,0 +1,96 @@
package com.ruoyi.wcs.domain.topology;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TopologyCompilerTest {
private final TopologyCompiler compiler = new TopologyCompiler();
@Test
void canonicalizesUndirectedEdgeResources() {
EffectiveTopology topology = compiler.compile(definition(
Arrays.asList(node("A", 0, 0), node("B", 1, 0)),
Collections.singletonList(new TopologyEdge("B", "A", 10, 1)),
Collections.<String>emptyList(), Collections.<String>emptyList()));
assertEquals("EDGE:A:B", topology.edge("A", "B").getResourceKey());
assertEquals("EDGE:A:B", topology.edge("B", "A").getResourceKey());
assertEquals(Collections.singletonList("B"), topology.neighbors("A"));
assertEquals(1, topology.resource("NODE:A").getCapacity());
}
@Test
void rejectsDisconnectedLiftPortReference() {
TopologyDefinition definition = definition(
Arrays.asList(node("A", 0, 0), node("B", 1, 0)),
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.singletonList("MISSING"), Collections.<String>emptyList());
DomainRuleViolation error = assertThrows(DomainRuleViolation.class,
() -> compiler.compile(definition));
assertEquals("lift port references unknown node: MISSING", error.getMessage());
}
@Test
void rejectsDuplicateCoordinatesOnTheSameFloor() {
TopologyDefinition definition = definition(
Arrays.asList(node("A", 0, 0), node("B", 0, 0)),
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.<String>emptyList(), Collections.<String>emptyList());
assertThrows(DomainRuleViolation.class, () -> compiler.compile(definition));
}
@Test
void rejectsUnconfirmedConfigurationAndNonPositiveCapacity() {
TopologyDefinition unconfirmed = new TopologyDefinition("FT546", 7L, false,
Collections.singletonList(node("A", 0, 0)),
Collections.<TopologyEdge>emptyList(),
Collections.<ResourceDefinition>emptyList(),
Collections.<String>emptyList(), Collections.<String>emptyList());
TopologyDefinition invalidCapacity = new TopologyDefinition("FT546", 7L, true,
Collections.singletonList(node("A", 0, 0)),
Collections.<TopologyEdge>emptyList(),
Collections.singletonList(new ResourceDefinition("NODE:A", 0)),
Collections.<String>emptyList(), Collections.<String>emptyList());
assertThrows(DomainRuleViolation.class, () -> compiler.compile(unconfirmed));
assertThrows(DomainRuleViolation.class, () -> compiler.compile(invalidCapacity));
}
@Test
void compiledTopologyDoesNotChangeWhenDefinitionCollectionsChange() {
List<TopologyNode> nodes = new ArrayList<TopologyNode>(
Arrays.asList(node("A", 0, 0), node("B", 1, 0)));
EffectiveTopology topology = compiler.compile(definition(nodes,
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.<String>emptyList(), Collections.<String>emptyList()));
nodes.clear();
assertTrue(topology.containsNode("A"));
assertThrows(UnsupportedOperationException.class,
() -> topology.neighbors("A").add("C"));
}
private static TopologyDefinition definition(List<TopologyNode> nodes,
List<TopologyEdge> edges,
List<String> liftPorts,
List<String> conveyorPorts) {
return new TopologyDefinition("FT546", 7L, true, nodes, edges,
Collections.<ResourceDefinition>emptyList(), liftPorts, conveyorPorts);
}
private static TopologyNode node(String id, int x, int y) {
return new TopologyNode(id, "F1", x, y, 1, false, false);
}
}