fix: repair shadow candidate planning and site runtime errors
This commit is contained in:
31
docs/FT546WMS-V34-REPAIR-0909-deploy.md
Normal file
31
docs/FT546WMS-V34-REPAIR-0909-deploy.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 2026-09-09 影子修复包切换
|
||||||
|
|
||||||
|
本包仍是 V3.4 数据库配套的 SHADOW 程序。无需执行数据库升级或卸载。Legacy 继续负责正式入出库。
|
||||||
|
|
||||||
|
## 现场操作
|
||||||
|
|
||||||
|
1. 在 IDEA 的运行配置列表确认能看到 `FT546WMS-V34-REPAIR-0909`。先不要启动。
|
||||||
|
2. 在协调好的时间停止当前 WMS:`FT546WMS-V34-HALFTRAY-FIX`。不要停止 WCS 或 WCS 影子程序。
|
||||||
|
3. 确认旧 WMS 完全退出,8080 不再监听。CMD 可执行 `netstat -ano | findstr :8080`;旧 PID 没有 LISTENING 才继续。
|
||||||
|
4. 选择 `FT546WMS-V34-REPAIR-0909`,点运行。等日志出现 `Started RuoYiApplication`,检查登录和查询。不要同时启动两个 WMS。
|
||||||
|
5. 记录启动时间,在正常业务发生后复核新的影子比较和电子屏发送情况。无需为测试制造业务单据。
|
||||||
|
|
||||||
|
新 JAR 目录:`C:\project\FT546WMS-v34-repair-20260909`。
|
||||||
|
|
||||||
|
继续使用服务器原有外置配置 `C:\project\FT546WMS-v34-jar-20260908\config`,不修改密码、数据库连接或影子开关。新增启动项不执行 Maven,不覆盖运行中的 JAR。
|
||||||
|
|
||||||
|
## 回退
|
||||||
|
|
||||||
|
如果新程序启动失败或影响操作,停止新 WMS,确认进程退出后启动旧配置 `FT546WMS-V34-HALFTRAY-FIX`。数据库没有变更,不需要回退 SQL。若需要完全关闭影子,使用已留存的 `FT546WMS-ROLLBACK-SOURCE-NO-BUILD`,先核对其 `FORTUCKY_SHADOW_ENABLED=false`。
|
||||||
|
|
||||||
|
## 本次修复与观察边界
|
||||||
|
|
||||||
|
- 出库从同代快照的候选库存行保留项目/物料,支持一个足量库存行位于多明细托盘。保留真实阻挡、锁定、预留、拓扑及数量校验。需要多个库存行凑量、候选跨多个项目/物料、已有托盘作业等仍明确标记不支持。
|
||||||
|
- 普通项目多物料入库采用项目级 OPEN 输入,不把任意一条明细当成全部。提前筛选 Legacy 保守可用货位后排名。
|
||||||
|
- 同一任务 spare1/2/3 有多组值时,只评估整条空巷道;记录 `READY_CONSERVATIVE` 与 `MATCH_CONSERVATIVE` / `DIFFERENT_CONSERVATIVE`。它们不是完整 Legacy 等价验证,验收时不能混入普通 MATCH。
|
||||||
|
- 出库原因新增候选证据、集合 SHA-256、完整性标记。`candidateEvidenceComplete=false` 表示原因字段空间不足,只保留部分明细,摘要不能代替完整历史回放。
|
||||||
|
- 非法 `stockinId[limit]` 请求返回参数格式提示且不查询明细;合法接口和服务端异常处理保留原行为。
|
||||||
|
- 电子屏两个定时任务独立持有消息和 socket;争用原 6666 端口时下一周期再发送,失败清理资源,保留 5 次协议重试。
|
||||||
|
- SQL 51025 仍表示源数据在刷新中变化而拒绝发布;不屏蔽该一致性保护,不增加影子刷新超时。
|
||||||
|
|
||||||
|
这次修复不等于批准新算法接管正式业务。两个历史出库代次已被覆盖,尚不能完整回放;请以本包启动后的新样本继续评估。至少按既有观察要求收集同版本有效出入库样本,并单独复核保守比较、跳过、差异和性能。
|
||||||
12
docs/superpowers/plans/2026-09-09-shadow-repair-plan.md
Normal file
12
docs/superpowers/plans/2026-09-09-shadow-repair-plan.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 现场修复实施计划
|
||||||
|
|
||||||
|
设计:../specs/2026-09-09-shadow-repair-design.md
|
||||||
|
|
||||||
|
- [x] 现场证据和根因调查;干净隔离工作树基线测试通过。
|
||||||
|
- [x] 添加查询绑定、UDP、入出库候选投影回归测试,确认失败。
|
||||||
|
- [x] 实现无数据库升级的修复;逐组测试。
|
||||||
|
- [x] 完整离线构建和全部 JUnit 测试,独立审查;审查修正后的最终构建成功。
|
||||||
|
- [ ] 生成版本化部署包并上传,新增启动项;不重启。
|
||||||
|
- [ ] 交付切换步骤、回退方式和仍需现场验证的边界。
|
||||||
|
|
||||||
|
后续上传与现场配置结果以独立交付目录中的 build-verification.json / site-preparation.json 为准,避免预先声明部署完成。
|
||||||
25
docs/superpowers/specs/2026-09-09-shadow-repair-design.md
Normal file
25
docs/superpowers/specs/2026-09-09-shadow-repair-design.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-09-09 现场问题修复
|
||||||
|
|
||||||
|
用户已授权修复现场检查发现的问题;继续 Legacy 执行业务、SHADOW 观察,准备交付但不重启现场。
|
||||||
|
|
||||||
|
## 证据与边界
|
||||||
|
|
||||||
|
现场 29 条记录中,5 条入库都不可比;3 次 Legacy 成功出库有 2 次影子缺货、1 次多明细跳过。历史库存代次已覆盖,修复机制需要回归测试和新样本,不宣称已重放两条历史作业。候选 SQL 的 include 已限制仓库及代次,不存在探查报告提到的跨仓跨代缺口。
|
||||||
|
|
||||||
|
## 设计
|
||||||
|
|
||||||
|
1. 出库增加仅在 SHADOW 可用的候选库存规划入口。传递快照筛选出的 stockId,以库存行投影项目/物料;保留巷道全部物理货位、阻挡、锁定、预留及缓冲条件。单行足量、多明细托盘可参与;多行合并数量、已有托盘作业仍明确不支持。限定最多 200 个候选。记录有界候选明细和完整集合摘要,不把 Legacy=10 当作匹配通过。
|
||||||
|
2. 入库在排名前限定同代 Legacy 保守可用货位。普通项目 OPEN 模式下,多物料/批次按项目级任务规划,不虚构单个物料。字段组唯一时保留严格同组或空巷道;字段组不唯一时只允许空巷道,单列保守比较结果,不作为完整 Legacy 等价验证。保留公共项目、范围不全、作业歧义等保护。
|
||||||
|
3. 查询参数绑定错误返回明确的参数格式提示,不执行无筛选明细查询。
|
||||||
|
4. 电子屏每次发送拥有独立消息和 socket。固定本地 6666 端口用非阻塞所有权保护,冲突时保留未发送状态待下个周期。try-with-resources 释放;socket 构造失败传播,接收信息为实例字段。保留屏幕协议、内容与重试次数。
|
||||||
|
5. SQL 51025 是源数据在刷新期间变化的拒绝保护;继续保留,不放松一致性或提高生产超时。本次不改数据库结构或生产过程。
|
||||||
|
|
||||||
|
## 验证与交付
|
||||||
|
|
||||||
|
先执行失败回归,再实现修复。覆盖真实 Spring 参数绑定、UDP 本机回环隔离/失败释放、混合托盘及阻挡、同代候选、入库分组/空巷道与保守比较。完整离线构建并显式 Surefire 执行 JUnit 5。独立代码审查后,新目录上传净化 JAR,新增 IDEA 启动项;核对摘要及现有进程未变化。用户择机停止旧 WMS 并切换,保留旧启动项回退。
|
||||||
|
|
||||||
|
## 核验和审查处置
|
||||||
|
|
||||||
|
2026-09-09 只读取得现场 `dbo.proc_allot_space`,普通项目两个分支均 UNION 整条空巷道,且均要求 `locked='N'`。不按共享模型对 0/null 的宽松解释放宽现场 SQL 谓词。完整定义保留本机 analysis/site-review-20260909/inbound-procedure.json。
|
||||||
|
|
||||||
|
独立审查要求保留瞬时 UDP 发送/接收错误的重试,已在协议循环内恢复有界重试并补回归;InvalidPropertyException 仅在实际 ModelAttribute 参数绑定且非法数组参数名来自请求时返回400,内部异常维持500,补真实 MockMvc 回归。候选跨多个项目/物料和多行凑量保留明确 skip;支持的是匹配单个项目/物料的库存行处于多项目/多物料托盘的情况,不声称覆盖任意多项目订单。
|
||||||
@@ -472,6 +472,7 @@ public final class InboundPlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean usableSlot(Lane lane, SpaceState space, InboundRequest request, InboundContext context) {
|
private boolean usableSlot(Lane lane, SpaceState space, InboundRequest request, InboundContext context) {
|
||||||
|
if (request.eligibleSpaceIds() != null && !request.eligibleSpaceIds().contains(space.spaceId())) return false;
|
||||||
if (!lane.usableForInbound() || !space.availableEmpty() || space.role() != SpaceRole.STORAGE) return false;
|
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 && space.floorNo() != request.targetFloor()) return false;
|
||||||
if (request.targetFloor() == null && !request.allowedFloors().isEmpty()
|
if (request.targetFloor() == null && !request.allowedFloors().isEmpty()
|
||||||
|
|||||||
@@ -12,8 +12,16 @@ public record InboundRequest(
|
|||||||
Integer entryFloor,
|
Integer entryFloor,
|
||||||
Integer targetFloor,
|
Integer targetFloor,
|
||||||
Set<Integer> allowedFloors,
|
Set<Integer> allowedFloors,
|
||||||
BatchLaneMode batchLaneMode
|
BatchLaneMode batchLaneMode,
|
||||||
|
Set<String> eligibleSpaceIds
|
||||||
) {
|
) {
|
||||||
|
public InboundRequest(String requestId, String trayNo, String projectNo, String productId, String lotNo,
|
||||||
|
Integer expectedProjectPallets, Integer entryFloor, Integer targetFloor,
|
||||||
|
Set<Integer> allowedFloors, BatchLaneMode batchLaneMode) {
|
||||||
|
this(requestId, trayNo, projectNo, productId, lotNo, expectedProjectPallets, entryFloor, targetFloor,
|
||||||
|
allowedFloors, batchLaneMode, null);
|
||||||
|
}
|
||||||
|
|
||||||
public InboundRequest {
|
public InboundRequest {
|
||||||
if (requestId == null || requestId.isBlank()) throw new IllegalArgumentException("requestId is required");
|
if (requestId == null || requestId.isBlank()) throw new IllegalArgumentException("requestId is required");
|
||||||
if (trayNo == null || trayNo.isBlank()) throw new IllegalArgumentException("trayNo is required");
|
if (trayNo == null || trayNo.isBlank()) throw new IllegalArgumentException("trayNo is required");
|
||||||
@@ -22,5 +30,7 @@ public record InboundRequest(
|
|||||||
}
|
}
|
||||||
allowedFloors = allowedFloors == null ? Set.of() : Set.copyOf(allowedFloors);
|
allowedFloors = allowedFloors == null ? Set.of() : Set.copyOf(allowedFloors);
|
||||||
batchLaneMode = batchLaneMode == null ? BatchLaneMode.OPEN : batchLaneMode;
|
batchLaneMode = batchLaneMode == null ? BatchLaneMode.OPEN : batchLaneMode;
|
||||||
|
// null means unrestricted; an empty captured set means no eligible destination.
|
||||||
|
eligibleSpaceIds = eligibleSpaceIds == null ? null : Set.copyOf(eligibleSpaceIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ public final class SchedulingFacade {
|
|||||||
return applicationService.planOutboundFromPublishedSnapshot(warehouseId, request);
|
return applicationService.planOutboundFromPublishedSnapshot(warehouseId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OutboundPlan planShadowOutboundCandidates(String warehouseId, OutboundRequest request,
|
||||||
|
java.util.Set<String> eligibleStockIds) {
|
||||||
|
if (properties.getMode() != SchedulingMode.SHADOW) {
|
||||||
|
throw new IllegalStateException("Captured candidate planning requires SHADOW mode");
|
||||||
|
}
|
||||||
|
return applicationService.planShadowOutboundCandidates(warehouseId, request, eligibleStockIds);
|
||||||
|
}
|
||||||
|
|
||||||
public TidyPlan planTidy(String warehouseId, Instant now) {
|
public TidyPlan planTidy(String warehouseId, Instant now) {
|
||||||
requireAlgorithmMode();
|
requireAlgorithmMode();
|
||||||
return applicationService.planTidy(warehouseId, now);
|
return applicationService.planTidy(warehouseId, now);
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ import java.util.Optional;
|
|||||||
public final class OutboundPlanner {
|
public final class OutboundPlanner {
|
||||||
|
|
||||||
public OutboundPlan plan(OutboundRequest request, OutboundContext context) {
|
public OutboundPlan plan(OutboundRequest request, OutboundContext context) {
|
||||||
|
return plan(request, context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutboundPlan plan(OutboundRequest request, OutboundContext context, java.util.Set<String> eligibleSpaceIds) {
|
||||||
String mode = normalizedMode(request.fifoMode());
|
String mode = normalizedMode(request.fifoMode());
|
||||||
|
if (eligibleSpaceIds != null && !"OFF".equals(mode)) {
|
||||||
|
throw new IllegalArgumentException("Captured candidate planning supports FIFO OFF only");
|
||||||
|
}
|
||||||
if (!List.of("OFF", "STRICT", "FLEXIBLE").contains(mode)) {
|
if (!List.of("OFF", "STRICT", "FLEXIBLE").contains(mode)) {
|
||||||
return error(OutboundErrorCode.FIFO_INVALID_MODE, "Unsupported FIFO mode",
|
return error(OutboundErrorCode.FIFO_INVALID_MODE, "Unsupported FIFO mode",
|
||||||
Map.of("fifoMode", String.valueOf(request.fifoMode())));
|
Map.of("fifoMode", String.valueOf(request.fifoMode())));
|
||||||
@@ -35,6 +42,7 @@ public final class OutboundPlanner {
|
|||||||
|
|
||||||
List<SpaceState> physicalMatches = context.spaces().stream()
|
List<SpaceState> physicalMatches = context.spaces().stream()
|
||||||
.filter(SpaceState::occupied)
|
.filter(SpaceState::occupied)
|
||||||
|
.filter(space -> eligibleSpaceIds == null || eligibleSpaceIds.contains(space.spaceId()))
|
||||||
.filter(space -> matchesRequest(space, request))
|
.filter(space -> matchesRequest(space, request))
|
||||||
.toList();
|
.toList();
|
||||||
if (physicalMatches.isEmpty()) {
|
if (physicalMatches.isEmpty()) {
|
||||||
|
|||||||
@@ -359,6 +359,17 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes) {
|
public List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes) {
|
||||||
|
return loadLaneSnapshots(warehouseId, laneCodes, Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LaneSnapshot> loadShadowOutboundLaneSnapshots(String warehouseId, Collection<String> laneCodes,
|
||||||
|
Set<String> candidateSpaceIds) {
|
||||||
|
return loadLaneSnapshots(warehouseId, laneCodes, Set.copyOf(candidateSpaceIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes,
|
||||||
|
Set<String> mixedProjectSources) {
|
||||||
LinkedHashSet<String> codes = laneCodes == null ? new LinkedHashSet<>()
|
LinkedHashSet<String> codes = laneCodes == null ? new LinkedHashSet<>()
|
||||||
: laneCodes.stream().filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
: laneCodes.stream().filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
if (codes.isEmpty()) return List.of();
|
if (codes.isEmpty()) return List.of();
|
||||||
@@ -393,7 +404,7 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
int capacity = entry.getValue().size();
|
int capacity = entry.getValue().size();
|
||||||
List<SpaceSnapshot> spaces = entry.getValue().stream()
|
List<SpaceSnapshot> spaces = entry.getValue().stream()
|
||||||
.map(raw -> mapSpace(raw, capacity, versions.get(entry.getKey()),
|
.map(raw -> mapSpace(raw, capacity, versions.get(entry.getKey()),
|
||||||
inventoryBySpace.getOrDefault(raw.spaceId(), List.of())))
|
inventoryBySpace.getOrDefault(raw.spaceId(), List.of()), mixedProjectSources.contains(raw.spaceId())))
|
||||||
.sorted(Comparator.comparingInt((SpaceSnapshot value) -> value.state().depthOrder())
|
.sorted(Comparator.comparingInt((SpaceSnapshot value) -> value.state().depthOrder())
|
||||||
.thenComparing(SpaceSnapshot::spaceId)).toList();
|
.thenComparing(SpaceSnapshot::spaceId)).toList();
|
||||||
result.add(new LaneSnapshot(warehouseId, entry.getKey(), versions.get(entry.getKey()), spaces));
|
result.add(new LaneSnapshot(warehouseId, entry.getKey(), versions.get(entry.getKey()), spaces));
|
||||||
@@ -720,6 +731,11 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SpaceSnapshot mapSpace(RawSpace raw, int capacity, VersionToken version, List<InventoryItem> inventory) {
|
private SpaceSnapshot mapSpace(RawSpace raw, int capacity, VersionToken version, List<InventoryItem> inventory) {
|
||||||
|
return mapSpace(raw, capacity, version, inventory, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpaceSnapshot mapSpace(RawSpace raw, int capacity, VersionToken version,
|
||||||
|
List<InventoryItem> inventory, boolean allowMixedProjectSource) {
|
||||||
EnumSet<SpaceConflict> conflicts = EnumSet.noneOf(SpaceConflict.class);
|
EnumSet<SpaceConflict> conflicts = EnumSet.noneOf(SpaceConflict.class);
|
||||||
if (raw.trayConflict()) conflicts.add(SpaceConflict.TRAY);
|
if (raw.trayConflict()) conflicts.add(SpaceConflict.TRAY);
|
||||||
if (raw.projectConflict()) conflicts.add(SpaceConflict.PROJECT);
|
if (raw.projectConflict()) conflicts.add(SpaceConflict.PROJECT);
|
||||||
@@ -734,7 +750,8 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
|||||||
Instant inboundAt = inventory.stream().map(InventoryItem::inboundAt).filter(Objects::nonNull)
|
Instant inboundAt = inventory.stream().map(InventoryItem::inboundAt).filter(Objects::nonNull)
|
||||||
.min(Comparator.naturalOrder()).orElse(null);
|
.min(Comparator.naturalOrder()).orElse(null);
|
||||||
boolean invalid = locked(raw.locked()) || raw.reserved() || raw.hasGap() || raw.depth() == null
|
boolean invalid = locked(raw.locked()) || raw.reserved() || raw.hasGap() || raw.depth() == null
|
||||||
|| !conflicts.isEmpty() || ("1".equals(raw.spaceStatus()) && (!inventory.isEmpty() || tray != null));
|
|| conflicts.stream().anyMatch(conflict -> conflict != SpaceConflict.PROJECT || !allowMixedProjectSource)
|
||||||
|
|| ("1".equals(raw.spaceStatus()) && (!inventory.isEmpty() || tray != null));
|
||||||
SpaceStatus status = invalid ? SpaceStatus.DISABLED : switch (Objects.toString(raw.spaceStatus(), "")) {
|
SpaceStatus status = invalid ? SpaceStatus.DISABLED : switch (Objects.toString(raw.spaceStatus(), "")) {
|
||||||
case "1" -> SpaceStatus.EMPTY;
|
case "1" -> SpaceStatus.EMPTY;
|
||||||
case "2" -> SpaceStatus.OCCUPIED;
|
case "2" -> SpaceStatus.OCCUPIED;
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ public interface WarehouseRepository {
|
|||||||
LanePage pageCandidateLanes(String warehouseId, String afterLaneCode, int pageSize);
|
LanePage pageCandidateLanes(String warehouseId, String afterLaneCode, int pageSize);
|
||||||
LanePage pageWarehouseLanes(String warehouseId, String afterLaneCode, int pageSize);
|
LanePage pageWarehouseLanes(String warehouseId, String afterLaneCode, int pageSize);
|
||||||
List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes);
|
List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes);
|
||||||
|
default List<LaneSnapshot> loadShadowOutboundLaneSnapshots(String warehouseId, Collection<String> laneCodes,
|
||||||
|
Set<String> candidateSpaceIds) {
|
||||||
|
return loadLaneSnapshots(warehouseId, laneCodes);
|
||||||
|
}
|
||||||
List<SpaceSnapshot> loadRelocationBuffers(String warehouseId, Set<Integer> floors);
|
List<SpaceSnapshot> loadRelocationBuffers(String warehouseId, Set<Integer> floors);
|
||||||
Set<Integer> loadActiveDeepWorkflowFloors(String warehouseId, String excludingRequestId);
|
Set<Integer> loadActiveDeepWorkflowFloors(String warehouseId, String excludingRequestId);
|
||||||
boolean acquireDeepWorkflowLock(String requestId, String warehouseId, int floorNo);
|
boolean acquireDeepWorkflowLock(String requestId, String warehouseId, int floorNo);
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ public final class WarehouseApplicationService {
|
|||||||
int currentProjectPallets = repository.countProjectPallets(warehouseId, request.projectNo());
|
int currentProjectPallets = repository.countProjectPallets(warehouseId, request.projectNo());
|
||||||
effectiveRequest = new InboundRequest(request.requestId(), request.trayNo(), request.projectNo(),
|
effectiveRequest = new InboundRequest(request.requestId(), request.trayNo(), request.projectNo(),
|
||||||
request.productId(), request.lotNo(), currentProjectPallets <= 1 ? 2 : 4,
|
request.productId(), request.lotNo(), currentProjectPallets <= 1 ? 2 : 4,
|
||||||
request.entryFloor(), request.targetFloor(), request.allowedFloors(), request.batchLaneMode());
|
request.entryFloor(), request.targetFloor(), request.allowedFloors(), request.batchLaneMode(),
|
||||||
|
request.eligibleSpaceIds());
|
||||||
}
|
}
|
||||||
List<PendingOutbound> pending = repository.loadPendingOutboundJobs(warehouseId).stream()
|
List<PendingOutbound> pending = repository.loadPendingOutboundJobs(warehouseId).stream()
|
||||||
.map(job -> new PendingOutbound(job.projectNo(), job.productNo(), job.trayNo(),
|
.map(job -> new PendingOutbound(job.projectNo(), job.productNo(), job.trayNo(),
|
||||||
@@ -136,6 +137,18 @@ public final class WarehouseApplicationService {
|
|||||||
return planOutbound(warehouseId, request, false);
|
return planOutbound(warehouseId, request, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The caller holds a published snapshot lease and supplies the site's eligible inventory IDs. */
|
||||||
|
public OutboundPlan planShadowOutboundCandidates(String warehouseId, OutboundRequest request,
|
||||||
|
Set<String> eligibleStockIds) {
|
||||||
|
Objects.requireNonNull(eligibleStockIds, "eligibleStockIds");
|
||||||
|
if (eligibleStockIds.size() > 200 || request.wholeTrayOrDefault() || nonBlank(request.trayNo())
|
||||||
|
|| !nonBlank(request.projectNo()) || !nonBlank(request.productId())
|
||||||
|
|| (nonBlank(request.fifoMode()) && !"OFF".equalsIgnoreCase(request.fifoMode()))) {
|
||||||
|
throw new IllegalArgumentException("Shadow candidates require a bounded project/product picking scope with FIFO OFF");
|
||||||
|
}
|
||||||
|
return planOutboundFromCurrentReadModel(warehouseId, request, Set.copyOf(eligibleStockIds));
|
||||||
|
}
|
||||||
|
|
||||||
private OutboundPlan planOutbound(String warehouseId, OutboundRequest request, boolean refresh) {
|
private OutboundPlan planOutbound(String warehouseId, OutboundRequest request, boolean refresh) {
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
repository.refreshChangedLanes(warehouseId);
|
repository.refreshChangedLanes(warehouseId);
|
||||||
@@ -144,6 +157,11 @@ public final class WarehouseApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private OutboundPlan planOutboundFromCurrentReadModel(String warehouseId, OutboundRequest request) {
|
private OutboundPlan planOutboundFromCurrentReadModel(String warehouseId, OutboundRequest request) {
|
||||||
|
return planOutboundFromCurrentReadModel(warehouseId, request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OutboundPlan planOutboundFromCurrentReadModel(String warehouseId, OutboundRequest request,
|
||||||
|
Set<String> eligibleStockIds) {
|
||||||
RepositoryPolicy policy = repository.loadPolicy(warehouseId);
|
RepositoryPolicy policy = repository.loadPolicy(warehouseId);
|
||||||
WarehouseSummary summary = repository.loadSummary(warehouseId);
|
WarehouseSummary summary = repository.loadSummary(warehouseId);
|
||||||
OutboundRequest effectiveRequest = request.fifoToleranceMinutes() == null && policy.fifoToleranceMinutes() > 0
|
OutboundRequest effectiveRequest = request.fifoToleranceMinutes() == null && policy.fifoToleranceMinutes() > 0
|
||||||
@@ -187,18 +205,28 @@ public final class WarehouseApplicationService {
|
|||||||
matches.addAll(repository.searchOutboundStock(warehouseId,
|
matches.addAll(repository.searchOutboundStock(warehouseId,
|
||||||
OutboundStockSearch.project(request.projectNo(), request.productId())));
|
OutboundStockSearch.project(request.projectNo(), request.productId())));
|
||||||
}
|
}
|
||||||
|
if (eligibleStockIds != null) {
|
||||||
|
matches.removeIf(match -> !eligibleStockIds.contains(match.stockId()));
|
||||||
|
Set<String> resolved = matches.stream().map(OutboundStockMatch::stockId).collect(java.util.stream.Collectors.toSet());
|
||||||
|
if (!resolved.equals(eligibleStockIds)) {
|
||||||
|
throw new IllegalStateException("Captured outbound candidate is missing from the published inventory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<String> matchedSpaceIds = matches.stream().map(OutboundStockMatch::spaceId)
|
||||||
|
.filter(this::nonBlank).collect(java.util.stream.Collectors.toSet());
|
||||||
List<String> laneCodes = matches.stream().map(OutboundStockMatch::laneCode).filter(Objects::nonNull)
|
List<String> laneCodes = matches.stream().map(OutboundStockMatch::laneCode).filter(Objects::nonNull)
|
||||||
.distinct().sorted().toList();
|
.distinct().sorted().toList();
|
||||||
List<LaneSnapshot> lanes = repository.loadLaneSnapshots(warehouseId, laneCodes);
|
List<LaneSnapshot> lanes = eligibleStockIds == null ? repository.loadLaneSnapshots(warehouseId, laneCodes)
|
||||||
|
: repository.loadShadowOutboundLaneSnapshots(warehouseId, laneCodes, matchedSpaceIds);
|
||||||
Set<Integer> floors = matches.stream().map(OutboundStockMatch::floorNo)
|
Set<Integer> floors = matches.stream().map(OutboundStockMatch::floorNo)
|
||||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||||
List<SpaceSnapshot> buffers = repository.loadRelocationBuffers(warehouseId, floors);
|
List<SpaceSnapshot> buffers = repository.loadRelocationBuffers(warehouseId, floors);
|
||||||
Map<String, SpaceState> uniqueSpaces = new LinkedHashMap<>();
|
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;
|
String productScope = matchedProductScope;
|
||||||
lanes.stream().flatMap(lane -> lane.spaceStates().stream()).forEach(space -> uniqueSpaces.put(space.spaceId(),
|
lanes.stream().flatMap(lane -> lane.spaceStates().stream()).forEach(space -> uniqueSpaces.put(space.spaceId(),
|
||||||
productScope != null && matchedSpaceIds.contains(space.spaceId())
|
eligibleStockIds != null && matchedSpaceIds.contains(space.spaceId())
|
||||||
|
? withInventoryScope(space, request.projectNo(), request.productId())
|
||||||
|
: productScope != null && matchedSpaceIds.contains(space.spaceId())
|
||||||
? withProductScope(space, productScope) : space));
|
? withProductScope(space, productScope) : space));
|
||||||
buffers.stream().map(SpaceSnapshot::state).forEach(space -> uniqueSpaces.put(space.spaceId(), space));
|
buffers.stream().map(SpaceSnapshot::state).forEach(space -> uniqueSpaces.put(space.spaceId(), space));
|
||||||
Map<String, String> rawInboundTimes = new LinkedHashMap<>();
|
Map<String, String> rawInboundTimes = new LinkedHashMap<>();
|
||||||
@@ -210,7 +238,8 @@ public final class WarehouseApplicationService {
|
|||||||
OutboundPlan plan = outboundPlanner.plan(effectiveRequest,
|
OutboundPlan plan = outboundPlanner.plan(effectiveRequest,
|
||||||
new OutboundContext(List.copyOf(uniqueSpaces.values()), rawInboundTimes,
|
new OutboundContext(List.copyOf(uniqueSpaces.values()), rawInboundTimes,
|
||||||
capacityMode(summary, policy),
|
capacityMode(summary, policy),
|
||||||
repository.loadActiveDeepWorkflowFloors(warehouseId, request.requestId())));
|
repository.loadActiveDeepWorkflowFloors(warehouseId, request.requestId())),
|
||||||
|
eligibleStockIds == null ? null : matchedSpaceIds);
|
||||||
String resultCode = switch (plan.status()) {
|
String resultCode = switch (plan.status()) {
|
||||||
case READY -> "READY";
|
case READY -> "READY";
|
||||||
case WAITING -> "WAITING_" + plan.waiting().orElseThrow().code().name();
|
case WAITING -> "WAITING_" + plan.waiting().orElseThrow().code().name();
|
||||||
@@ -312,10 +341,14 @@ public final class WarehouseApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SpaceState withProductScope(SpaceState space, String productId) {
|
private SpaceState withProductScope(SpaceState space, String productId) {
|
||||||
|
return withInventoryScope(space, space.projectNo(), productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpaceState withInventoryScope(SpaceState space, String projectNo, String productId) {
|
||||||
return new SpaceState(space.spaceId(), space.zoneCode(), space.laneCode(), space.depthOrder(),
|
return new SpaceState(space.spaceId(), space.zoneCode(), space.laneCode(), space.depthOrder(),
|
||||||
space.laneCapacity(), space.floorNo(), space.aisleNo(), space.groupCoding(), space.rowCode(),
|
space.laneCapacity(), space.floorNo(), space.aisleNo(), space.groupCoding(), space.rowCode(),
|
||||||
space.role(), space.status(), space.outboundTaskActive(), space.inventoryProjectConflict(),
|
space.role(), space.status(), space.outboundTaskActive(), space.inventoryProjectConflict(),
|
||||||
space.trayNo(), space.projectNo(), productId, space.lotNo(), space.inboundAt(), space.outboundAt(),
|
space.trayNo(), projectNo, productId, space.lotNo(), space.inboundAt(), space.outboundAt(),
|
||||||
space.reserved(), space.rowVersion(), space.physicalStockPresent());
|
space.reserved(), space.rowVersion(), space.physicalStockPresent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,33 @@ class JdbcWarehouseRepositoryTest {
|
|||||||
assertThat(repository.countProjectPallets("W1", "PROJECT-W2")).isZero();
|
assertThat(repository.countProjectPallets("W1", "PROJECT-W2")).isZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mixedProjectPalletIsSelectableByItsEligibleInventoryItem() throws Exception {
|
||||||
|
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('MIXED-SOURCE','W1','R','A','Mixed pallet',9,91,1,'2','',"
|
||||||
|
+ "'MIXED-TRAY','MIXED-LANE','1')");
|
||||||
|
execute("INSERT INTO BAS_STOCK VALUES ('MIX-1','MIXED-SOURCE','MIXED-TRAY','P1','SKU1','LOT','2026-01-01 09:00:00',5,0)");
|
||||||
|
execute("INSERT INTO BAS_STOCK VALUES ('MIX-2','MIXED-SOURCE','MIXED-TRAY','P2','SKU2','LOT','2026-01-01 09:00:00',5,0)");
|
||||||
|
rebuildTopology("W1");
|
||||||
|
var service = new WarehouseApplicationService(repository, new InboundPlanner(), new OutboundPlanner(), new TidyPlanner(), 100);
|
||||||
|
var request = new OutboundRequest("MIX-PICK", null, "P1", "SKU1", "OFF", null, 1, false);
|
||||||
|
|
||||||
|
var plan = service.planShadowOutboundCandidates("W1", request, Set.of("MIX-1"));
|
||||||
|
|
||||||
|
assertThat(plan.status()).isEqualTo(OutboundPlanStatus.READY);
|
||||||
|
assertThat(plan.assignment().orElseThrow().sourceSpaceId()).isEqualTo("MIXED-SOURCE");
|
||||||
|
assertThat(plan.assignment().orElseThrow().projectNo()).isEqualTo("P1");
|
||||||
|
assertThat(plan.assignment().orElseThrow().productId()).isEqualTo("SKU1");
|
||||||
|
assertThat(space(repository.loadLaneSnapshots("W1", List.of("MIXED-LANE")), "MIXED-SOURCE")
|
||||||
|
.state().outboundSelectable()).isFalse();
|
||||||
|
for (String columnValue : List.of("locked='Y'", "status='4'", "tray_code='WRONG'")) {
|
||||||
|
execute("UPDATE BAS_CARGO_SPACE SET " + columnValue + " WHERE cargo_space_id='MIXED-SOURCE'");
|
||||||
|
rebuildTopology("W1");
|
||||||
|
assertThat(service.planShadowOutboundCandidates("W1", request, Set.of("MIX-1")).status())
|
||||||
|
.isNotEqualTo(OutboundPlanStatus.READY);
|
||||||
|
execute("UPDATE BAS_CARGO_SPACE SET locked='',status='2',tray_code='MIXED-TRAY' WHERE cargo_space_id='MIXED-SOURCE'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void statusFourPhysicalOuterPalletIsPreservedAndMakesDeepOutboundWait() throws Exception {
|
void statusFourPhysicalOuterPalletIsPreservedAndMakesDeepOutboundWait() throws Exception {
|
||||||
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('ALLOC-TARGET','W1','RA','A','Target',7,21,2,'2','',"
|
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('ALLOC-TARGET','W1','RA','A','Target',7,21,2,'2','',"
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ruoyi.algorithm.service;
|
||||||
|
|
||||||
|
import com.ruoyi.algorithm.inbound.*;
|
||||||
|
import com.ruoyi.algorithm.integration.*;
|
||||||
|
import com.ruoyi.algorithm.model.*;
|
||||||
|
import com.ruoyi.algorithm.outbound.*;
|
||||||
|
import com.ruoyi.algorithm.repository.*;
|
||||||
|
import com.ruoyi.algorithm.tidy.TidyPlanner;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
|
||||||
|
class ShadowCandidatePlanningTest {
|
||||||
|
@Test
|
||||||
|
void ineligibleOuterStockRemainsAPhysicalBlockerInsteadOfBeingChosenOrRemoved() {
|
||||||
|
var repository = mock(WarehouseRepository.class);
|
||||||
|
when(repository.loadPolicy("01")).thenReturn(RepositoryPolicy.defaults());
|
||||||
|
when(repository.loadSummary("01")).thenReturn(new WarehouseSummary("01", 1, 2, 2, 0, 0, 0, 0));
|
||||||
|
when(repository.searchOutboundStock(eq("01"), any())).thenReturn(List.of(
|
||||||
|
new OutboundStockMatch("INNER", "L-1", "L", 1, "T1", "P", "SKU", "LOT", null, null),
|
||||||
|
new OutboundStockMatch("OUTER", "L-2", "L", 1, "T2", "P", "SKU", "LOT", null, null)));
|
||||||
|
var source = state("L", 1, SpaceStatus.OCCUPIED, "T1", null);
|
||||||
|
var blocker = state("L", 2, SpaceStatus.OCCUPIED, "T2", "P");
|
||||||
|
when(repository.loadShadowOutboundLaneSnapshots("01", List.of("L"), Set.of("L-1")))
|
||||||
|
.thenReturn(List.of(lane("L", source, blocker)));
|
||||||
|
var service = new WarehouseApplicationService(repository, new InboundPlanner(), new OutboundPlanner(), new TidyPlanner(), 100);
|
||||||
|
var request = new OutboundRequest("O", null, "P", "SKU", "OFF", null, 1, false);
|
||||||
|
|
||||||
|
var plan = service.planShadowOutboundCandidates("01", request, Set.of("INNER"));
|
||||||
|
|
||||||
|
assertThat(plan.waiting()).isPresent();
|
||||||
|
assertThat(plan.waiting().orElseThrow().code()).isEqualTo(OutboundWaitingCode.WAITING_RELOCATION_BUFFER);
|
||||||
|
assertThat(plan.waiting().orElseThrow().details().get("sourceSpaceId")).isEqualTo("L-1");
|
||||||
|
verify(repository, never()).synchronizeReadModel(any());
|
||||||
|
verify(repository, never()).refreshChangedLanes(any());
|
||||||
|
verify(repository, never()).tryReserveInbound(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void eligibleInboundSetSurvivesProjectSizeResolutionAndPageBoundaries() {
|
||||||
|
var repository = mock(WarehouseRepository.class);
|
||||||
|
when(repository.loadPolicy("01")).thenReturn(RepositoryPolicy.defaults());
|
||||||
|
when(repository.loadSummary("01")).thenReturn(new WarehouseSummary("01", 2, 4, 0, 4, 0, 0, 0));
|
||||||
|
when(repository.pageCandidateLanes("01", null, 1)).thenReturn(new LanePage(
|
||||||
|
List.of(new LaneCandidate("01", "A", 2, 0, 2, false, false, VersionToken.numeric(1))), "A"));
|
||||||
|
when(repository.pageCandidateLanes("01", "A", 1)).thenReturn(new LanePage(
|
||||||
|
List.of(new LaneCandidate("01", "Z", 2, 0, 2, false, false, VersionToken.numeric(2))), null));
|
||||||
|
when(repository.loadLaneSnapshots("01", List.of("A"))).thenReturn(List.of(lane("A",
|
||||||
|
state("A", 1, SpaceStatus.EMPTY, null, null), state("A", 2, SpaceStatus.EMPTY, null, null))));
|
||||||
|
when(repository.loadLaneSnapshots("01", List.of("Z"))).thenReturn(List.of(lane("Z",
|
||||||
|
state("Z", 1, SpaceStatus.EMPTY, null, null), state("Z", 2, SpaceStatus.EMPTY, null, null))));
|
||||||
|
var service = new WarehouseApplicationService(repository, new InboundPlanner(), new OutboundPlanner(), new TidyPlanner(), 1);
|
||||||
|
var request = new InboundRequest("I", "T", "P", null, null, null, 1, null, Set.of(), BatchLaneMode.OPEN, Set.of("Z-1"));
|
||||||
|
|
||||||
|
assertThat(service.planInboundFromPublishedSnapshot("01", request).plan().assignment().orElseThrow().spaceId())
|
||||||
|
.isEqualTo("Z-1");
|
||||||
|
var forbidden = new InboundRequest("I", "T", "P", null, null, null, 1, null, Set.of(), BatchLaneMode.OPEN, Set.of());
|
||||||
|
assertThat(service.planInboundFromPublishedSnapshot("01", forbidden).plan().assignment()).isEmpty();
|
||||||
|
verify(repository, never()).refreshChangedLanes(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void candidateProjectionCannotBeCalledThroughLegacyOrActiveFacade() {
|
||||||
|
for (var mode : List.of(SchedulingMode.LEGACY, SchedulingMode.ACTIVE)) {
|
||||||
|
var props = new AlgorithmProperties();
|
||||||
|
props.setMode(mode);
|
||||||
|
var service = mock(WarehouseApplicationService.class);
|
||||||
|
var facade = new SchedulingFacade(service, props);
|
||||||
|
assertThatThrownBy(() -> facade.planShadowOutboundCandidates("01",
|
||||||
|
new OutboundRequest("O", null, "P", "SKU", "OFF", null, 1, false), Set.of("S")))
|
||||||
|
.isInstanceOf(IllegalStateException.class).hasMessageContaining("SHADOW");
|
||||||
|
verifyNoInteractions(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LaneSnapshot lane(String code, SpaceState... spaces) {
|
||||||
|
return new LaneSnapshot("01", code, VersionToken.numeric(1), java.util.Arrays.stream(spaces)
|
||||||
|
.map(s -> new SpaceSnapshot(s, List.of(), Set.of(), s.availableEmpty(), false, null)).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpaceState state(String lane, int depth, SpaceStatus status, String tray, String project) {
|
||||||
|
return new SpaceState(lane + "-" + depth, "A", lane, depth, 2, 1, 1, depth, "1", SpaceRole.STORAGE,
|
||||||
|
status, false, false, tray, project, "SKU", "LOT", Instant.EPOCH, null, false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.ruoyi.Dzp;
|
package com.ruoyi.Dzp;
|
||||||
|
|
||||||
|
|
||||||
import com.ruoyi.Dzp.domain.basLed;
|
import com.ruoyi.Dzp.domain.basLed;
|
||||||
import com.ruoyi.Dzp.mapper.BasLedMapper;
|
import com.ruoyi.Dzp.mapper.BasLedMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -9,7 +8,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api2dzp")
|
@RequestMapping("/api2dzp")
|
||||||
@@ -17,104 +16,46 @@ public class DzpSend {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BasLedMapper mapper;
|
private BasLedMapper mapper;
|
||||||
|
|
||||||
|
// Both scheduled tasks use the device's existing local port. A busy port is retried next tick.
|
||||||
|
private final ReentrantLock portOwner = new ReentrantLock();
|
||||||
|
|
||||||
@Scheduled(fixedDelay = 1000)
|
@Scheduled(fixedDelay = 1000)
|
||||||
public void InLed() throws IOException {
|
public void InLed() throws IOException { sendPending(false); }
|
||||||
basLed basLed = new basLed();
|
|
||||||
basLed.setSendFlag("0");
|
|
||||||
basLed.setId(1);
|
|
||||||
List<com.ruoyi.Dzp.domain.basLed> basLeds = mapper.selectBasLedList(basLed);
|
|
||||||
if (basLeds != null && basLeds.size() > 0 && (basLeds.get(0).getModel().equals("1") || basLeds.get(0).getModel().equals("2"))) {
|
|
||||||
MyUdpSocket myUdpSocket = new MyUdpSocket(6666);
|
|
||||||
ProtocolDemo.my_udp = myUdpSocket;
|
|
||||||
String ip = basLeds.get(0).getIpAddress();
|
|
||||||
String msgProductTitle = "物资编码:";
|
|
||||||
String msgProduct = basLeds.get(0).getProduct_id();
|
|
||||||
String msgSpecTitle = "物资规格:";
|
|
||||||
String msgSpec = basLeds.get(0).getSpec();
|
|
||||||
String msg3 = "托盘条码:" + basLeds.get(0).getTrayCode();
|
|
||||||
String msg4 = null;
|
|
||||||
if (basLeds.get(0).getModel().equals("1")) {
|
|
||||||
msg4 = "入库数量:";
|
|
||||||
} else {
|
|
||||||
msg4 = "出库数量:";
|
|
||||||
}
|
|
||||||
String ms7 = "物资名称:";
|
|
||||||
String msg8 = basLeds.get(0).getProductName();
|
|
||||||
String msgQuantity = basLeds.get(0).getQuantity();
|
|
||||||
if (ProtocolDemo.demo_play_chapterschedule4(ip, msgProductTitle, msgSpecTitle, msg4, msg3, msgSpec, msgProduct, ms7, msg8, msgQuantity)) {
|
|
||||||
basLeds.get(0).setSendFlag("1");
|
|
||||||
mapper.updateBasLed(basLeds.get(0));
|
|
||||||
}
|
|
||||||
myUdpSocket.close();
|
|
||||||
|
|
||||||
}
|
|
||||||
basLed basLed2 = new basLed();
|
|
||||||
basLed2.setSendFlag("0");
|
|
||||||
basLed2.setId(2);
|
|
||||||
List<com.ruoyi.Dzp.domain.basLed> basLeds2 = mapper.selectBasLedList(basLed2);
|
|
||||||
if (basLeds2 != null && basLeds2.size() > 0 && (basLeds2.get(0).getModel().equals("1") || basLeds2.get(0).getModel().equals("2"))) {
|
|
||||||
MyUdpSocket myUdpSocket = new MyUdpSocket(6666);
|
|
||||||
ProtocolDemo.my_udp = myUdpSocket;
|
|
||||||
|
|
||||||
String ip = basLeds2.get(0).getIpAddress();
|
|
||||||
String msgProductTitle = "物资编码:";
|
|
||||||
String msgProduct = basLeds2.get(0).getProduct_id();
|
|
||||||
String msgSpecTitle = "物资规格:";
|
|
||||||
String msgSpec = basLeds2.get(0).getSpec();
|
|
||||||
String msg3 = "托盘条码:" + basLeds2.get(0).getTrayCode();
|
|
||||||
String msg4 = null;
|
|
||||||
if (basLeds2.get(0).getModel().equals("1")) {
|
|
||||||
msg4 = "入库数量:";
|
|
||||||
} else {
|
|
||||||
msg4 = "出库数量:";
|
|
||||||
}
|
|
||||||
String ms7 = "物资名称:";
|
|
||||||
String msg8 = basLeds2.get(0).getProductName();
|
|
||||||
String msgQuantity = basLeds2.get(0).getQuantity();
|
|
||||||
if (ProtocolDemo.demo_play_chapterschedule4(ip, msgProductTitle, msgSpecTitle, msg4, msg3, msgSpec, msgProduct, ms7, msg8, msgQuantity)) {
|
|
||||||
basLeds2.get(0).setSendFlag("1");
|
|
||||||
mapper.updateBasLed(basLeds2.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
myUdpSocket.close();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Scheduled(fixedDelay = 1000)
|
@Scheduled(fixedDelay = 1000)
|
||||||
public void ErrorLed() throws IOException {
|
public void ErrorLed() throws IOException { sendPending(true); }
|
||||||
basLed basLed = new basLed();
|
|
||||||
basLed.setSendFlag("0");
|
|
||||||
basLed.setId(1);
|
|
||||||
List<com.ruoyi.Dzp.domain.basLed> basLeds = mapper.selectBasLedList(basLed);
|
|
||||||
if (basLeds != null && basLeds.size() > 0 && basLeds.get(0).getModel().equals("3")) {
|
|
||||||
MyUdpSocket myUdpSocket = new MyUdpSocket(6666);
|
|
||||||
ProtocolDemo.my_udp = myUdpSocket;
|
|
||||||
String ip = basLeds.get(0).getIpAddress();
|
|
||||||
if (ProtocolDemo.demo_play_chapterschedule2(ip, "报警信息", basLeds.get(0).getErrorMsg())) {
|
|
||||||
basLeds.get(0).setSendFlag("1");
|
|
||||||
mapper.updateBasLed(basLeds.get(0));
|
|
||||||
}
|
|
||||||
myUdpSocket.close();
|
|
||||||
|
|
||||||
}
|
private void sendPending(boolean alarm) throws IOException {
|
||||||
basLed basLed2 = new basLed();
|
if (!portOwner.tryLock()) return;
|
||||||
basLed2.setSendFlag("0");
|
try {
|
||||||
basLed2.setId(2);
|
for (int id = 1; id <= 2; id++) {
|
||||||
List<com.ruoyi.Dzp.domain.basLed> basLeds2 = mapper.selectBasLedList(basLed2);
|
basLed query = new basLed();
|
||||||
if (basLeds2 != null && basLeds2.size() > 0 && basLeds2.get(0).getModel().equals("3")) {
|
query.setId(id);
|
||||||
MyUdpSocket myUdpSocket = new MyUdpSocket(6666);
|
query.setSendFlag("0");
|
||||||
ProtocolDemo.my_udp = myUdpSocket;
|
var records = mapper.selectBasLedList(query);
|
||||||
String ip = basLeds2.get(0).getIpAddress();
|
if (records == null || records.isEmpty()) continue;
|
||||||
if (ProtocolDemo.demo_play_chapterschedule2(ip, "报警信息", basLeds2.get(0).getErrorMsg())) {
|
basLed record = records.get(0);
|
||||||
basLeds2.get(0).setSendFlag("1");
|
boolean matchingModel = alarm ? "3".equals(record.getModel())
|
||||||
mapper.updateBasLed(basLeds2.get(0));
|
: "1".equals(record.getModel()) || "2".equals(record.getModel());
|
||||||
|
if (!matchingModel) continue;
|
||||||
|
boolean sent;
|
||||||
|
try (MyUdpSocket socket = openSocket()) {
|
||||||
|
sent = alarm
|
||||||
|
? ProtocolDemo.demo_play_chapterschedule2(socket, record.getIpAddress(), "报警信息", record.getErrorMsg())
|
||||||
|
: ProtocolDemo.demo_play_chapterschedule4(socket, record.getIpAddress(), "物资编码:", "物资规格:",
|
||||||
|
"1".equals(record.getModel()) ? "入库数量:" : "出库数量:",
|
||||||
|
"托盘条码:" + record.getTrayCode(), record.getSpec(), record.getProduct_id(),
|
||||||
|
"物资名称:", record.getProductName(), record.getQuantity());
|
||||||
|
}
|
||||||
|
if (sent) {
|
||||||
|
record.setSendFlag("1");
|
||||||
|
mapper.updateBasLed(record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
myUdpSocket.close();
|
} finally {
|
||||||
|
portOwner.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MyUdpSocket openSocket() throws IOException { return new MyUdpSocket(6666); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,88 +6,73 @@ import java.nio.ByteBuffer;
|
|||||||
import java.nio.channels.DatagramChannel;
|
import java.nio.channels.DatagramChannel;
|
||||||
import java.nio.channels.SelectionKey;
|
import java.nio.channels.SelectionKey;
|
||||||
import java.nio.channels.Selector;
|
import java.nio.channels.Selector;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class MyUdpSocket {
|
/** One exchange owns its channel, selector, receive buffer and peer address. */
|
||||||
private DatagramChannel channel;
|
public class MyUdpSocket implements AutoCloseable {
|
||||||
private Selector selector;
|
private final DatagramChannel channel;
|
||||||
private final int MAX_SIZE = 1280;
|
private final Selector selector;
|
||||||
private static int client_port;
|
private int clientPort;
|
||||||
private static String client_ip;
|
private String clientIp;
|
||||||
ByteBuffer receive_buffer = ByteBuffer.allocate(MAX_SIZE);
|
private final ByteBuffer receiveBuffer = ByteBuffer.allocate(1280);
|
||||||
|
|
||||||
public MyUdpSocket(int port) throws IOException {
|
public MyUdpSocket(int port) throws IOException {
|
||||||
channel = DatagramChannel.open();
|
channel = DatagramChannel.open();
|
||||||
selector = Selector.open();
|
Selector opened = null;
|
||||||
try {
|
try {
|
||||||
//<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģʽ
|
opened = Selector.open();
|
||||||
channel.configureBlocking(false);
|
channel.configureBlocking(false);
|
||||||
//<2F><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><D7BD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><D7BD>֣<EFBFBD><D6A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><D7BD>ְ<D6B0><F3B6A8B5><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>˿<EFBFBD>
|
channel.bind(new InetSocketAddress(port));
|
||||||
channel.socket().bind(new InetSocketAddress(port));
|
channel.register(opened, SelectionKey.OP_READ);
|
||||||
//Ϊͨ<CEAA><CDA8>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD>
|
selector = opened;
|
||||||
channel.register(selector, SelectionKey.OP_READ);
|
} catch (IOException | RuntimeException failure) {
|
||||||
} catch (IOException e) {
|
if (opened != null) {
|
||||||
e.printStackTrace();
|
try { opened.close(); } catch (IOException close) { failure.addSuppressed(close); }
|
||||||
|
}
|
||||||
|
try { channel.close(); } catch (IOException close) { failure.addSuppressed(close); }
|
||||||
|
throw failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void send(String ip, int port, byte[] buffer, int size) throws IOException {
|
public final void send(String ip, int port, byte[] buffer, int size) throws IOException {
|
||||||
InetSocketAddress client = new InetSocketAddress(ip, port);
|
channel.send(ByteBuffer.wrap(buffer, 0, size), new InetSocketAddress(ip, port));
|
||||||
channel.send(ByteBuffer.wrap(buffer, 0, size), client); //<2F><>EchoClient<6E>ظ<EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD>ݱ<EFBFBD>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public final void close() throws IOException {
|
public final void close() throws IOException {
|
||||||
selector.close();
|
try { selector.close(); } finally { channel.close(); }
|
||||||
channel.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final int get_client_port() {
|
public final int get_client_port() { return clientPort; }
|
||||||
return client_port;
|
public final String get_client_ip() { return clientIp; }
|
||||||
}
|
|
||||||
|
|
||||||
public final String get_client_ip() {
|
|
||||||
return client_ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void get_receive_buffer(byte[] buffer) {
|
public final void get_receive_buffer(byte[] buffer) {
|
||||||
receive_buffer.get(buffer);
|
get_receive_packet(buffer, buffer.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final int receive() {
|
public final int receive() {
|
||||||
try {
|
try {
|
||||||
receive_buffer.clear();
|
receiveBuffer.clear();
|
||||||
if (selector.select(10) != 0) {
|
if (selector.select(10) != 0) {
|
||||||
Set<SelectionKey> readyKeys = selector.selectedKeys();
|
var keys = selector.selectedKeys().iterator();
|
||||||
for (SelectionKey key : readyKeys) {
|
while (keys.hasNext()) {
|
||||||
readyKeys.remove(key);
|
SelectionKey key = keys.next();
|
||||||
if (key.isReadable()) {
|
keys.remove();
|
||||||
DatagramChannel dc = (DatagramChannel) key.channel();
|
if (!key.isValid() || !key.isReadable()) continue;
|
||||||
InetSocketAddress client = (InetSocketAddress) dc.receive(receive_buffer); //<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>Client<6E><74><EFBFBD><EFBFBD><EFBFBD>ݱ<EFBFBD>
|
InetSocketAddress peer = (InetSocketAddress) channel.receive(receiveBuffer);
|
||||||
if (client != null) {
|
if (peer != null) {
|
||||||
client_ip = client.getAddress().getHostAddress();
|
clientIp = peer.getAddress().getHostAddress();
|
||||||
client_port = client.getPort();
|
clientPort = peer.getPort();
|
||||||
//System.out.println(client_ip+":"+client_port+" size="+receive_buffer.position());
|
return receiveBuffer.position();
|
||||||
key.interestOps(SelectionKey.OP_READ);
|
|
||||||
return receive_buffer.position();
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
throw new java.io.UncheckedIOException("LED receive failed", e);
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void get_receive_packet(byte[] packet, int size) {
|
public final void get_receive_packet(byte[] packet, int size) {
|
||||||
int i;
|
System.arraycopy(receiveBuffer.array(), 0, packet, 0, size);
|
||||||
for (i = 0; i < size; i++) {
|
|
||||||
packet[i] = receive_buffer.array()[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.awt.*;
|
|||||||
//import java.util.Calendar;
|
//import java.util.Calendar;
|
||||||
|
|
||||||
public class ProtocolDemo {
|
public class ProtocolDemo {
|
||||||
|
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProtocolDemo.class);
|
||||||
|
|
||||||
// ====<3D><><EFBFBD>붯<EFBFBD><EBB6AF><EFBFBD><EFBFBD>ʽ<EFBFBD>б<EFBFBD>(<28><>ֵ<EFBFBD><D6B5>0<EFBFBD><30>ʼ)====
|
// ====<3D><><EFBFBD>붯<EFBFBD><EBB6AF><EFBFBD><EFBFBD>ʽ<EFBFBD>б<EFBFBD>(<28><>ֵ<EFBFBD><D6B5>0<EFBFBD><30>ʼ)====
|
||||||
// 0 = '<27><><EFBFBD>',
|
// 0 = '<27><><EFBFBD>',
|
||||||
@@ -101,10 +102,19 @@ public class ProtocolDemo {
|
|||||||
|
|
||||||
//<2F><><EFBFBD><EFBFBD>UDP<44><50><EFBFBD><EFBFBD>bufferΪ<72><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݣ<EFBFBD>sizeΪ<65><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݳ<EFBFBD><DDB3><EFBFBD>
|
//<2F><><EFBFBD><EFBFBD>UDP<44><50><EFBFBD><EFBFBD>bufferΪ<72><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݣ<EFBFBD>sizeΪ<65><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݳ<EFBFBD><DDB3><EFBFBD>
|
||||||
public static int udp_receive(byte[] buffer) {
|
public static int udp_receive(byte[] buffer) {
|
||||||
|
return udp_receive(my_udp, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int udp_receive(MyUdpSocket socket, byte[] buffer) {
|
||||||
int size = 0;
|
int size = 0;
|
||||||
size = my_udp.receive();
|
try {
|
||||||
|
size = socket.receive();
|
||||||
|
} catch (java.io.UncheckedIOException e) {
|
||||||
|
log.debug("LED receive attempt failed; the bounded exchange will retry", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
my_udp.get_receive_packet(buffer, size);
|
socket.get_receive_packet(buffer, size);
|
||||||
}
|
}
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
@@ -415,6 +425,20 @@ public class ProtocolDemo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean send_data1(String ip) {
|
public static boolean send_data1(String ip) {
|
||||||
|
return send_data1(my_udp, ledsender, ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean udp_send1(MyUdpSocket socket, String ip, byte[] buffer, int size) {
|
||||||
|
try {
|
||||||
|
socket.send(ip, ledport, buffer, size);
|
||||||
|
return true;
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
log.debug("LED send attempt failed; the bounded exchange will retry", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean send_data1(MyUdpSocket socket, LEDSender2010 ledsender, String ip) {
|
||||||
int size = 0;
|
int size = 0;
|
||||||
byte[] packet = new byte[1280];
|
byte[] packet = new byte[1280];
|
||||||
int i, j, k, x, r;
|
int i, j, k, x, r;
|
||||||
@@ -435,9 +459,9 @@ public class ProtocolDemo {
|
|||||||
ok = false;
|
ok = false;
|
||||||
for (tx = 0; tx < tx_repeat; tx++) {
|
for (tx = 0; tx < tx_repeat; tx++) {
|
||||||
size = ledsender.pkg_begin(packet, addr);
|
size = ledsender.pkg_begin(packet, addr);
|
||||||
udp_send1(ip, packet, size);
|
if (!udp_send1(socket, ip, packet, size)) continue;
|
||||||
for (x = 0; x < 50; x++) {
|
for (x = 0; x < 50; x++) {
|
||||||
size = udp_receive(packet);
|
size = udp_receive(socket, packet);
|
||||||
if (size > 0 && ledsender.parse_trans_respond(packet, size, LEDSender2010.PKC_BEGIN, 0) > 0) {
|
if (size > 0 && ledsender.parse_trans_respond(packet, size, LEDSender2010.PKC_BEGIN, 0) > 0) {
|
||||||
ok = true;
|
ok = true;
|
||||||
break;
|
break;
|
||||||
@@ -459,9 +483,9 @@ public class ProtocolDemo {
|
|||||||
for (tx = 0; tx < tx_repeat; tx++) {
|
for (tx = 0; tx < tx_repeat; tx++) {
|
||||||
j = i;
|
j = i;
|
||||||
size = ledsender.pkg_data(packet, addr, i, 512);
|
size = ledsender.pkg_data(packet, addr, i, 512);
|
||||||
udp_send1(ip, packet, size);
|
if (!udp_send1(socket, ip, packet, size)) continue;
|
||||||
for (x = 0; x < 50; x++) {
|
for (x = 0; x < 50; x++) {
|
||||||
size = udp_receive(packet);
|
size = udp_receive(socket, packet);
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
r = ledsender.parse_trans_respond(packet, size,LEDSender2010.PKC_DATA, i);
|
r = ledsender.parse_trans_respond(packet, size,LEDSender2010.PKC_DATA, i);
|
||||||
switch (r) {
|
switch (r) {
|
||||||
@@ -490,9 +514,9 @@ public class ProtocolDemo {
|
|||||||
ok = false;
|
ok = false;
|
||||||
for (tx = 0; tx < tx_repeat; tx++) {
|
for (tx = 0; tx < tx_repeat; tx++) {
|
||||||
size = ledsender.pkg_end(packet, addr, i);
|
size = ledsender.pkg_end(packet, addr, i);
|
||||||
udp_send1(ip, packet, size);
|
if (!udp_send1(socket, ip, packet, size)) continue;
|
||||||
for (x = 0; x < 50; x++) {
|
for (x = 0; x < 50; x++) {
|
||||||
size = udp_receive(packet);
|
size = udp_receive(socket, packet);
|
||||||
if (size > 0 && ledsender.parse_trans_respond(packet, size, LEDSender2010.PKC_END, i) > 0) {
|
if (size > 0 && ledsender.parse_trans_respond(packet, size, LEDSender2010.PKC_END, i) > 0) {
|
||||||
ok = true;
|
ok = true;
|
||||||
break;
|
break;
|
||||||
@@ -870,6 +894,11 @@ public class ProtocolDemo {
|
|||||||
|
|
||||||
//<2F>Զ<EFBFBD><D4B6>巽<EFBFBD><E5B7BD> <20><>̬<EFBFBD><CCAC>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
//<2F>Զ<EFBFBD><D4B6>巽<EFBFBD><E5B7BD> <20><>̬<EFBFBD><CCAC>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||||
public static boolean demo_play_chapterschedule2(String ip, String msg1, String msg2) {
|
public static boolean demo_play_chapterschedule2(String ip, String msg1, String msg2) {
|
||||||
|
return demo_play_chapterschedule2(my_udp, ip, msg1, msg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean demo_play_chapterschedule2(MyUdpSocket socket, String ip, String msg1, String msg2) {
|
||||||
|
LEDSender2010 ledsender = new LEDSender2010();
|
||||||
|
|
||||||
//<2F><><EFBFBD>ɽ<EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>
|
//<2F><><EFBFBD>ɽ<EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>
|
||||||
ledsender.MakeRoot(LEDSender2010.ROOT_PLAY,LEDSender2010.COLOR_TYPE_DOUBLE);
|
ledsender.MakeRoot(LEDSender2010.ROOT_PLAY,LEDSender2010.COLOR_TYPE_DOUBLE);
|
||||||
@@ -882,7 +911,7 @@ public class ProtocolDemo {
|
|||||||
ledsender.AddText(0, 11, 192, 32, msg1, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>", 16, Color.BLACK, Color.YELLOW, 0, 2, false, 1, 0, 2, 0, 0, 5, 10000);
|
ledsender.AddText(0, 11, 192, 32, msg1, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>", 16, Color.BLACK, Color.YELLOW, 0, 2, false, 1, 0, 2, 0, 0, 5, 10000);
|
||||||
ledsender.AddText(0, 37, 192, 32, msg2, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>", 16, Color.BLACK, Color.YELLOW, 0, 2, false, 1, 0, 2, 0, 0, 5, 10000);
|
ledsender.AddText(0, 37, 192, 32, msg2, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>", 16, Color.BLACK, Color.YELLOW, 0, 2, false, 1, 0, 2, 0, 0, 5, 10000);
|
||||||
|
|
||||||
return send_data1(ip);
|
return send_data1(socket, ledsender, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
//<2F>Զ<EFBFBD><D4B6>巽<EFBFBD><E5B7BD><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
//<2F>Զ<EFBFBD><D4B6>巽<EFBFBD><E5B7BD><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
@@ -931,6 +960,11 @@ public class ProtocolDemo {
|
|||||||
|
|
||||||
|
|
||||||
public static boolean demo_play_chapterschedule4(String ip, String msg1, String msg2, String msg3, String msg,String msg5,String msg6,String msg7,String msg8, String msgQuantity) {
|
public static boolean demo_play_chapterschedule4(String ip, String msg1, String msg2, String msg3, String msg,String msg5,String msg6,String msg7,String msg8, String msgQuantity) {
|
||||||
|
return demo_play_chapterschedule4(my_udp, ip, msg1, msg2, msg3, msg, msg5, msg6, msg7, msg8, msgQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean demo_play_chapterschedule4(MyUdpSocket socket, String ip, String msg1, String msg2, String msg3, String msg, String msg5, String msg6, String msg7, String msg8, String msgQuantity) {
|
||||||
|
LEDSender2010 ledsender = new LEDSender2010();
|
||||||
|
|
||||||
//<2F><><EFBFBD>ɽ<EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>
|
//<2F><><EFBFBD>ɽ<EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>
|
||||||
ledsender.MakeRoot(LEDSender2010.ROOT_PLAY,LEDSender2010.COLOR_TYPE_DOUBLE);
|
ledsender.MakeRoot(LEDSender2010.ROOT_PLAY,LEDSender2010.COLOR_TYPE_DOUBLE);
|
||||||
@@ -951,7 +985,7 @@ public class ProtocolDemo {
|
|||||||
ledsender.AddText(65, 60, 192, 21, msg8, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>", 13, Color.BLACK, Color.YELLOW, 0, 2, false, 1, 0, 7, 0, 0, 5, 3000);
|
ledsender.AddText(65, 60, 192, 21, msg8, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>", 13, Color.BLACK, Color.YELLOW, 0, 2, false, 1, 0, 7, 0, 0, 5, 3000);
|
||||||
|
|
||||||
|
|
||||||
return send_data1(ip);
|
return send_data1(socket, ledsender, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.ruoyi.algorithm.shadow;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Bounded evidence in the existing decision reason; no live business reads or extra DB writes. */
|
||||||
|
final class ShadowCandidateEvidence {
|
||||||
|
private static final ObjectMapper JSON = new ObjectMapper();
|
||||||
|
|
||||||
|
static String describe(List<ShadowStockReference> candidates) {
|
||||||
|
var rows = candidates.stream().map(c -> new Candidate(c.getStockId(), c.getSpaceId(), c.getTrayNo(),
|
||||||
|
c.getProjectNo(), c.getProductId(), c.getAvailableQuantity(), c.getTrayItemCount(), c.getTrayJobCount(),
|
||||||
|
c.getSpare1(), c.getSpare2(), c.getSpare3(), c.getVoltage(), c.getPhase())).toList();
|
||||||
|
try {
|
||||||
|
String hash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(JSON.writeValueAsBytes(rows)));
|
||||||
|
List<Candidate> retained = new ArrayList<>();
|
||||||
|
String detail = "[]";
|
||||||
|
for (Candidate row : rows) {
|
||||||
|
retained.add(row);
|
||||||
|
String proposed = JSON.writeValueAsString(retained);
|
||||||
|
if (proposed.length() > 1100) { retained.remove(retained.size() - 1); break; }
|
||||||
|
detail = proposed;
|
||||||
|
}
|
||||||
|
return "candidateCount=" + rows.size() + "; candidateSha256=" + hash
|
||||||
|
+ "; candidateEvidenceComplete=" + (retained.size() == rows.size())
|
||||||
|
+ "; candidateEvidence=" + detail;
|
||||||
|
} catch (JsonProcessingException | NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("Could not capture outbound candidate evidence", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Candidate(String stock, String space, String tray, String project, String product,
|
||||||
|
BigDecimal availableQuantity, Integer trayItems, Integer trayJobs,
|
||||||
|
String spare1, String spare2, String spare3, String voltage, String phase) { }
|
||||||
|
}
|
||||||
@@ -99,6 +99,13 @@ public class ShadowPlanningEngine {
|
|||||||
}
|
}
|
||||||
List<ShadowStockReference> candidates = inputMapper.selectProjectOutboundCandidates(
|
List<ShadowStockReference> candidates = inputMapper.selectProjectOutboundCandidates(
|
||||||
warehouseId, snapshot.generation(), stockoutDetailId);
|
warehouseId, snapshot.generation(), stockoutDetailId);
|
||||||
|
return withCandidateEvidence(planProjectCandidates(snapshot, stockoutDetailId, reference, candidates), candidates);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private OutboundShadowObservation planProjectCandidates(PublishedShadowSnapshot snapshot, String stockoutDetailId,
|
||||||
|
ShadowStockReference reference, List<ShadowStockReference> candidates) {
|
||||||
|
String warehouseId = snapshot.warehouseId();
|
||||||
if (candidates.size() > MAX_OUTBOUND_CANDIDATES) {
|
if (candidates.size() > MAX_OUTBOUND_CANDIDATES) {
|
||||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
"SKIPPED_CANDIDATE_LIMIT", "Eligible candidate set exceeds 200 rows");
|
"SKIPPED_CANDIDATE_LIMIT", "Eligible candidate set exceeds 200 rows");
|
||||||
@@ -117,16 +124,33 @@ public class ShadowPlanningEngine {
|
|||||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
"SKIPPED_UNSUPPORTED_PRODUCT_SCOPE", "Planner requires one nonblank candidate product");
|
"SKIPPED_UNSUPPORTED_PRODUCT_SCOPE", "Planner requires one nonblank candidate product");
|
||||||
}
|
}
|
||||||
// The shared planner is single-source and does not apply the site's optional predicates.
|
List<ShadowStockReference> idle = candidates.stream()
|
||||||
// Keep it unchanged; validate its proposal against the generation-qualified candidate set.
|
.filter(candidate -> Integer.valueOf(0).equals(candidate.getTrayJobCount())).toList();
|
||||||
|
if (idle.isEmpty()) {
|
||||||
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
|
"SKIPPED_EXISTING_TRAY_JOB", "Existing tray jobs require unsupported delivery compatibility checks");
|
||||||
|
}
|
||||||
|
List<ShadowStockReference> sufficient = idle.stream().filter(candidate -> candidate.getAvailableQuantity() != null
|
||||||
|
&& candidate.getAvailableQuantity().compareTo(reference.getRequestedQuantity()) >= 0).toList();
|
||||||
|
if (sufficient.isEmpty()) {
|
||||||
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
|
"SKIPPED_MULTI_STOCK_REQUEST", "No single eligible stock row covers the remaining quantity");
|
||||||
|
}
|
||||||
|
if (sufficient.stream().anyMatch(candidate -> isBlank(candidate.getStockId()) || isBlank(candidate.getSpaceId())
|
||||||
|
|| isBlank(candidate.getTrayNo()) || candidate.getTrayItemCount() == null || candidate.getTrayItemCount() < 1)
|
||||||
|
|| sufficient.stream().map(ShadowStockReference::getSpaceId).distinct().count() != sufficient.size()) {
|
||||||
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
|
"SKIPPED_AMBIGUOUS_STOCK_SCOPE", "Each eligible source must identify exactly one sufficient stock row");
|
||||||
|
}
|
||||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
||||||
projects.get(0), products.get(0), null, null, null, false);
|
projects.get(0), products.get(0), null, null, null, false);
|
||||||
OutboundPlan plan = schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request);
|
OutboundPlan plan = schedulingFacade.planShadowOutboundCandidates(warehouseId, request,
|
||||||
|
sufficient.stream().map(ShadowStockReference::getStockId).collect(java.util.stream.Collectors.toSet()));
|
||||||
if (plan.assignment().isEmpty()) {
|
if (plan.assignment().isEmpty()) {
|
||||||
return toOutboundObservation(snapshot, stockoutDetailId, reference, plan);
|
return toOutboundObservation(snapshot, stockoutDetailId, reference, plan);
|
||||||
}
|
}
|
||||||
var assignment = plan.assignment().orElseThrow();
|
var assignment = plan.assignment().orElseThrow();
|
||||||
var selected = candidates.stream().filter(candidate ->
|
var selected = sufficient.stream().filter(candidate ->
|
||||||
Objects.equals(candidate.getSpaceId(), assignment.sourceSpaceId())
|
Objects.equals(candidate.getSpaceId(), assignment.sourceSpaceId())
|
||||||
&& Objects.equals(candidate.getTrayNo(), assignment.trayNo())
|
&& Objects.equals(candidate.getTrayNo(), assignment.trayNo())
|
||||||
&& Objects.equals(candidate.getProjectNo(), assignment.projectNo())
|
&& Objects.equals(candidate.getProjectNo(), assignment.projectNo())
|
||||||
@@ -140,10 +164,6 @@ public class ShadowPlanningEngine {
|
|||||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
"SKIPPED_EXISTING_TRAY_JOB", "Existing tray jobs require unsupported delivery compatibility checks");
|
"SKIPPED_EXISTING_TRAY_JOB", "Existing tray jobs require unsupported delivery compatibility checks");
|
||||||
}
|
}
|
||||||
if (!Integer.valueOf(1).equals(stock.getTrayItemCount())) {
|
|
||||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
|
||||||
"SKIPPED_MULTI_STOCK_TRAY", "Selected tray does not contain exactly one inventory row");
|
|
||||||
}
|
|
||||||
if (stock.getAvailableQuantity() == null
|
if (stock.getAvailableQuantity() == null
|
||||||
|| stock.getAvailableQuantity().compareTo(reference.getRequestedQuantity()) < 0) {
|
|| stock.getAvailableQuantity().compareTo(reference.getRequestedQuantity()) < 0) {
|
||||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||||
@@ -157,7 +177,14 @@ public class ShadowPlanningEngine {
|
|||||||
return new OutboundShadowObservation(true, snapshot.generation(), warehouseId,
|
return new OutboundShadowObservation(true, snapshot.generation(), warehouseId,
|
||||||
snapshot.synchronizedAt(), snapshot.sourceSignature(), stockoutDetailId,
|
snapshot.synchronizedAt(), snapshot.sourceSignature(), stockoutDetailId,
|
||||||
"READY", stock.getStockId(), stock.getSpaceId(), reason.toString());
|
"READY", stock.getStockId(), stock.getSpaceId(), reason.toString());
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private OutboundShadowObservation withCandidateEvidence(OutboundShadowObservation observation,
|
||||||
|
List<ShadowStockReference> candidates) {
|
||||||
|
return new OutboundShadowObservation(observation.observed(), observation.snapshotGeneration(),
|
||||||
|
observation.warehouseId(), observation.snapshotSynchronizedAt(), observation.sourceSignature(),
|
||||||
|
observation.requestId(), observation.resultCode(), observation.proposedStockId(), observation.proposedSpaceId(),
|
||||||
|
abbreviate(observation.reason(), 1900) + "; " + ShadowCandidateEvidence.describe(candidates));
|
||||||
}
|
}
|
||||||
|
|
||||||
private OutboundShadowObservation unavailableProject(PublishedShadowSnapshot snapshot, String requestId,
|
private OutboundShadowObservation unavailableProject(PublishedShadowSnapshot snapshot, String requestId,
|
||||||
@@ -195,8 +222,7 @@ public class ShadowPlanningEngine {
|
|||||||
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
||||||
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||||
observation.sourceSignature(),
|
observation.sourceSignature(),
|
||||||
observation.observed() && "READY".equals(observation.resultCode()) && "10".equals(legacyResultCode)
|
inboundComparisonCode(observation, legacyResultCode, legacySpaceId),
|
||||||
? comparisonCode(observation.proposedSpaceId(), legacySpaceId) : "NOT_COMPARABLE",
|
|
||||||
inboundComparisonReason(observation, legacyResultCode, legacySpaceId));
|
inboundComparisonReason(observation, legacyResultCode, legacySpaceId));
|
||||||
requireComparisonInserted(inserted);
|
requireComparisonInserted(inserted);
|
||||||
});
|
});
|
||||||
@@ -244,22 +270,29 @@ public class ShadowPlanningEngine {
|
|||||||
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_MISSING_TASK_DETAILS",
|
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_MISSING_TASK_DETAILS",
|
||||||
"Job was captured but has no task details");
|
"Job was captured but has no task details");
|
||||||
}
|
}
|
||||||
if (!Integer.valueOf(1).equals(reference.getFieldGroupCount())) {
|
if (reference.getFieldGroupCount() == null || reference.getFieldGroupCount() < 1
|
||||||
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_AMBIGUOUS_LEGACY_FIELDS",
|
|| reference.getProductScopeCount() == null || reference.getProductScopeCount() < 1) {
|
||||||
"Legacy TOP (1) has no ordering and task details have different spare1/2/3 values");
|
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_MISSING_PREDICATE_INPUT",
|
||||||
}
|
"Task field or product scope counts are missing");
|
||||||
if (!Integer.valueOf(1).equals(reference.getProductScopeCount()) || isBlank(reference.getProductId())) {
|
|
||||||
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_MULTI_PRODUCT_INPUT",
|
|
||||||
"Single-product planner cannot represent this task product/lot scope");
|
|
||||||
}
|
}
|
||||||
if (isBlank(reference.getProjectNo()) || "999".equals(reference.getProjectNo().trim())
|
if (isBlank(reference.getProjectNo()) || "999".equals(reference.getProjectNo().trim())
|
||||||
|| "COMMON".equalsIgnoreCase(reference.getProjectNo().trim())) {
|
|| "COMMON".equalsIgnoreCase(reference.getProjectNo().trim())) {
|
||||||
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_UNSUPPORTED_COMMON_PROJECT",
|
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_UNSUPPORTED_COMMON_PROJECT",
|
||||||
"Common-project mapping and pool policy require separate validation");
|
"Common-project mapping and pool policy require separate validation");
|
||||||
}
|
}
|
||||||
|
boolean ambiguousFields = reference.getFieldGroupCount() > 1;
|
||||||
|
boolean multipleProducts = reference.getProductScopeCount() > 1;
|
||||||
|
List<String> eligibleSpaces = inputMapper.selectInboundEligibleSpaces(warehouseId,
|
||||||
|
snapshot.generation(), taskNo, trayNo);
|
||||||
|
if (eligibleSpaces.isEmpty()) {
|
||||||
|
return unavailableInbound(snapshot, taskNo, trayNo, reference, "SKIPPED_NO_CONSERVATIVE_SPACE",
|
||||||
|
ambiguousFields ? "No fully empty lane; Legacy TOP (1) field choice remains unknown"
|
||||||
|
: "No space satisfies the captured conservative Legacy predicates");
|
||||||
|
}
|
||||||
InboundRequest request = new InboundRequest(reference.getRequestId(), reference.getTrayNo(),
|
InboundRequest request = new InboundRequest(reference.getRequestId(), reference.getTrayNo(),
|
||||||
reference.getProjectNo(), reference.getProductId(), reference.getLotNo(),
|
reference.getProjectNo(), multipleProducts ? null : reference.getProductId(),
|
||||||
null, null, null, Set.of(), null);
|
multipleProducts ? null : reference.getLotNo(),
|
||||||
|
null, null, null, Set.of(), null, Set.copyOf(eligibleSpaces));
|
||||||
InboundPlanningResult result = schedulingFacade.planInboundFromPublishedSnapshot(warehouseId, request);
|
InboundPlanningResult result = schedulingFacade.planInboundFromPublishedSnapshot(warehouseId, request);
|
||||||
InboundPlan plan = result.plan();
|
InboundPlan plan = result.plan();
|
||||||
if (plan.assignment().isPresent()) {
|
if (plan.assignment().isPresent()) {
|
||||||
@@ -272,9 +305,11 @@ public class ShadowPlanningEngine {
|
|||||||
}
|
}
|
||||||
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
||||||
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||||
reference.getRequestId(), "READY", assignment.spaceId(),
|
reference.getRequestId(), ambiguousFields ? "READY_CONSERVATIVE" : "READY", assignment.spaceId(),
|
||||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
||||||
"legacyEligibility=STRICT_GROUP_OR_EMPTY; " + assignment.reason()));
|
"legacyEligibility=" + (ambiguousFields ? "EMPTY_LANE_ONLY" : "STRICT_GROUP_OR_EMPTY")
|
||||||
|
+ "; inputScope=" + (multipleProducts ? "PROJECT_MULTI_ITEM_OPEN" : "SINGLE_ITEM")
|
||||||
|
+ "; eligibleSpaceCount=" + eligibleSpaces.size() + "; " + assignment.reason()));
|
||||||
}
|
}
|
||||||
var rejection = plan.rejection().orElseThrow();
|
var rejection = plan.rejection().orElseThrow();
|
||||||
return InboundShadowObservation.unavailable(snapshot,
|
return InboundShadowObservation.unavailable(snapshot,
|
||||||
@@ -555,6 +590,16 @@ public class ShadowPlanningEngine {
|
|||||||
return shadowSpaceId.equals(legacySpaceId) ? "MATCH" : "DIFFERENT";
|
return shadowSpaceId.equals(legacySpaceId) ? "MATCH" : "DIFFERENT";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String inboundComparisonCode(InboundShadowObservation observation, String legacyCode, String legacySpace) {
|
||||||
|
if (!observation.observed() || !"10".equals(legacyCode)) return "NOT_COMPARABLE";
|
||||||
|
if (!"READY".equals(observation.resultCode()) && !"READY_CONSERVATIVE".equals(observation.resultCode())) {
|
||||||
|
return "NOT_COMPARABLE";
|
||||||
|
}
|
||||||
|
String result = comparisonCode(observation.proposedSpaceId(), legacySpace);
|
||||||
|
return "READY_CONSERVATIVE".equals(observation.resultCode()) && !"NOT_COMPARABLE".equals(result)
|
||||||
|
? result + "_CONSERVATIVE" : result;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isBlank(String value) {
|
private boolean isBlank(String value) {
|
||||||
return value == null || value.isBlank();
|
return value == null || value.isBlank();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ public interface ShadowInputMapper {
|
|||||||
@Param("generation") long generation, @Param("taskNo") String taskNo,
|
@Param("generation") long generation, @Param("taskNo") String taskNo,
|
||||||
@Param("trayNo") String trayNo, @Param("spaceId") String spaceId);
|
@Param("trayNo") String trayNo, @Param("spaceId") String spaceId);
|
||||||
|
|
||||||
|
List<String> selectInboundEligibleSpaces(@Param("warehouseId") String warehouseId,
|
||||||
|
@Param("generation") long generation, @Param("taskNo") String taskNo,
|
||||||
|
@Param("trayNo") String trayNo);
|
||||||
|
|
||||||
ShadowStockReference selectProjectOutboundReference(@Param("warehouseId") String warehouseId,
|
ShadowStockReference selectProjectOutboundReference(@Param("warehouseId") String warehouseId,
|
||||||
@Param("generation") long generation,
|
@Param("generation") long generation,
|
||||||
@Param("stockoutDetailId") String stockoutDetailId);
|
@Param("stockoutDetailId") String stockoutDetailId);
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<!-- Conservative subset valid in either Legacy project-count branch. Ranking remains in the planner. -->
|
<!-- Conservative subset valid in either Legacy project-count branch. Ranking remains in the planner. -->
|
||||||
<select id="countInboundEligibleSpace" resultType="int">
|
<select id="countInboundEligibleSpace" resultType="int">
|
||||||
select count(*)
|
select count(*)
|
||||||
|
<include refid="inboundEligibleSpaces"/>
|
||||||
|
and cargo.space_id = #{spaceId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectInboundEligibleSpaces" resultType="string">
|
||||||
|
select cargo.space_id
|
||||||
|
<include refid="inboundEligibleSpaces"/>
|
||||||
|
order by cargo.space_id
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<sql id="inboundEligibleSpaces">
|
||||||
from alg.shadow_inbound_request_snapshot input
|
from alg.shadow_inbound_request_snapshot input
|
||||||
join alg.cargo_space_snapshot cargo
|
join alg.cargo_space_snapshot cargo
|
||||||
on cargo.warehouse_id = input.warehouse_id
|
on cargo.warehouse_id = input.warehouse_id
|
||||||
@@ -39,11 +50,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
where input.warehouse_id = #{warehouseId} and input.snapshot_generation = #{generation}
|
where input.warehouse_id = #{warehouseId} and input.snapshot_generation = #{generation}
|
||||||
and input.task_no = #{taskNo} and input.tray_no = #{trayNo}
|
and input.task_no = #{taskNo} and input.tray_no = #{trayNo}
|
||||||
and input.predicate_version = 1 and input.detail_count > 0
|
and input.predicate_version = 1 and input.detail_count > 0
|
||||||
and input.field_group_count = 1 and input.product_scope_count = 1
|
and input.field_group_count > 0 and input.product_scope_count > 0
|
||||||
and input.tray_job_count = 1 and input.legacy_scope_complete = 1
|
and input.tray_job_count = 1 and input.legacy_scope_complete = 1
|
||||||
and nullif(ltrim(rtrim(input.project_no)), '') is not null
|
and nullif(ltrim(rtrim(input.project_no)), '') is not null
|
||||||
and input.project_no not in ('999', 'COMMON')
|
and input.project_no not in ('999', 'COMMON')
|
||||||
and cargo.space_id = #{spaceId} and cargo.space_status = '1' and cargo.locked = 'N'
|
and cargo.space_status = '1' and cargo.locked = 'N'
|
||||||
and nullif(ltrim(rtrim(cargo.tray_code)), '') is null
|
and nullif(ltrim(rtrim(cargo.tray_code)), '') is null
|
||||||
and cargo.this_cargo_area in ('A', 'C')
|
and cargo.this_cargo_area in ('A', 'C')
|
||||||
and nullif(ltrim(rtrim(cargo.lane_code)), '') is not null
|
and nullif(ltrim(rtrim(cargo.lane_code)), '') is not null
|
||||||
@@ -68,7 +79,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
or exists (select 1 from alg.inventory_item_snapshot stock
|
or exists (select 1 from alg.inventory_item_snapshot stock
|
||||||
where stock.warehouse_id = lane.warehouse_id
|
where stock.warehouse_id = lane.warehouse_id
|
||||||
and stock.snapshot_generation = lane.snapshot_generation and stock.space_id = lane.space_id)))
|
and stock.snapshot_generation = lane.snapshot_generation and stock.space_id = lane.space_id)))
|
||||||
or exists (
|
or (input.field_group_count = 1 and exists (
|
||||||
select 1 from alg.cargo_space_snapshot resident
|
select 1 from alg.cargo_space_snapshot resident
|
||||||
join alg.inventory_item_snapshot stock
|
join alg.inventory_item_snapshot stock
|
||||||
on stock.warehouse_id = resident.warehouse_id and stock.snapshot_generation = resident.snapshot_generation
|
on stock.warehouse_id = resident.warehouse_id and stock.snapshot_generation = resident.snapshot_generation
|
||||||
@@ -77,8 +88,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
and resident.lane_code = cargo.lane_code and resident.space_status = '2'
|
and resident.lane_code = cargo.lane_code and resident.space_status = '2'
|
||||||
and resident.this_cargo_area in ('A', 'C')
|
and resident.this_cargo_area in ('A', 'C')
|
||||||
and stock.project_no = input.project_no
|
and stock.project_no = input.project_no
|
||||||
and stock.spare1 = input.spare1 and stock.spare2 = input.spare2 and stock.spare3 = input.spare3))
|
and stock.spare1 = input.spare1 and stock.spare2 = input.spare2 and stock.spare3 = input.spare3)))
|
||||||
</select>
|
</sql>
|
||||||
|
|
||||||
<sql id="outboundCandidatePredicate">
|
<sql id="outboundCandidatePredicate">
|
||||||
item.warehouse_id = #{warehouseId}
|
item.warehouse_id = #{warehouseId}
|
||||||
@@ -130,6 +141,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
select top (201)
|
select top (201)
|
||||||
item.stock_id as stockId, item.tray_code as trayNo, item.space_id as spaceId,
|
item.stock_id as stockId, item.tray_code as trayNo, item.space_id as spaceId,
|
||||||
item.project_no as projectNo, item.product_no as productId,
|
item.project_no as projectNo, item.product_no as productId,
|
||||||
|
item.spare1, item.spare2, item.spare3, item.voltage, item.phase,
|
||||||
item.tray_job_count as trayJobCount,
|
item.tray_job_count as trayJobCount,
|
||||||
item.quantity - item.out_quantity as availableQuantity,
|
item.quantity - item.out_quantity as availableQuantity,
|
||||||
(select count(*) from alg.inventory_item_snapshot trayItem
|
(select count(*) from alg.inventory_item_snapshot trayItem
|
||||||
|
|||||||
98
ruoyi-admin/src/test/java/com/ruoyi/Dzp/DzpSendTest.java
Normal file
98
ruoyi-admin/src/test/java/com/ruoyi/Dzp/DzpSendTest.java
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ruoyi.Dzp;
|
||||||
|
|
||||||
|
import com.ruoyi.Dzp.domain.basLed;
|
||||||
|
import com.ruoyi.Dzp.mapper.BasLedMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
|
||||||
|
class DzpSendTest {
|
||||||
|
@Test
|
||||||
|
void failedExchangeClosesItsSocketAndDoesNotAcknowledgeTheDisplay() throws Exception {
|
||||||
|
var socket = mock(MyUdpSocket.class);
|
||||||
|
var sender = new DzpSend() { @Override MyUdpSocket openSocket() { return socket; } };
|
||||||
|
var mapper = mock(BasLedMapper.class);
|
||||||
|
ReflectionTestUtils.setField(sender, "mapper", mapper);
|
||||||
|
var record = new basLed(); record.setModel("3"); record.setSendFlag("0"); record.setIpAddress("127.0.0.1"); record.setErrorMsg("alarm");
|
||||||
|
when(mapper.selectBasLedList(any())).thenAnswer(call -> call.getArgument(0, basLed.class).getId() == 1 ? List.of(record) : List.of());
|
||||||
|
try (var protocol = mockStatic(ProtocolDemo.class)) {
|
||||||
|
protocol.when(() -> ProtocolDemo.demo_play_chapterschedule2(socket, "127.0.0.1", "报警信息", "alarm"))
|
||||||
|
.thenThrow(new IllegalStateException("test send failure")).thenReturn(true);
|
||||||
|
assertThatThrownBy(sender::ErrorLed).hasMessage("test send failure");
|
||||||
|
verify(socket).close();
|
||||||
|
verify(mapper, never()).updateBasLed(any());
|
||||||
|
assertThat(record.getSendFlag()).isEqualTo("0");
|
||||||
|
sender.ErrorLed();
|
||||||
|
verify(socket, times(2)).close();
|
||||||
|
verify(mapper).updateBasLed(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void busyLocalPortDefersOtherTimerWithoutBlockingOrChangingRows() throws Exception {
|
||||||
|
var entered = new CountDownLatch(1);
|
||||||
|
var release = new CountDownLatch(1);
|
||||||
|
var sender = new DzpSend() {
|
||||||
|
@Override MyUdpSocket openSocket() throws IOException {
|
||||||
|
entered.countDown();
|
||||||
|
try { if (!release.await(3, TimeUnit.SECONDS)) throw new IOException("test timeout"); }
|
||||||
|
catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(e); }
|
||||||
|
throw new IOException("fixture port failure");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var mapper = mock(BasLedMapper.class);
|
||||||
|
ReflectionTestUtils.setField(sender, "mapper", mapper);
|
||||||
|
var record = new basLed(); record.setModel("1"); record.setSendFlag("0");
|
||||||
|
when(mapper.selectBasLedList(any())).thenReturn(List.of(record));
|
||||||
|
var executor = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
var first = executor.submit(() -> { assertThatThrownBy(sender::InLed).isInstanceOf(IOException.class); });
|
||||||
|
assertThat(entered.await(2, TimeUnit.SECONDS)).isTrue();
|
||||||
|
executor.submit(() -> { sender.ErrorLed(); return null; }).get(1, TimeUnit.SECONDS);
|
||||||
|
verify(mapper, times(1)).selectBasLedList(any());
|
||||||
|
verify(mapper, never()).updateBasLed(any());
|
||||||
|
release.countDown();
|
||||||
|
first.get(2, TimeUnit.SECONDS);
|
||||||
|
} finally { release.countDown(); executor.shutdownNow(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void eachScheduledMessageBuildsItsOwnPayload() throws Exception {
|
||||||
|
try (var payloads = mockConstruction(LEDSender2010.class,
|
||||||
|
(sender, context) -> when(sender.GetDataSize()).thenReturn((1 << 21) + 1))) {
|
||||||
|
var socket = mock(MyUdpSocket.class);
|
||||||
|
assertThat(ProtocolDemo.demo_play_chapterschedule2(socket, "127.0.0.1", "title1", "one")).isFalse();
|
||||||
|
assertThat(ProtocolDemo.demo_play_chapterschedule2(socket, "127.0.0.1", "title2", "two")).isFalse();
|
||||||
|
assertThat(payloads.constructed()).hasSize(2);
|
||||||
|
verify(payloads.constructed().get(0)).GetDataSize();
|
||||||
|
verify(payloads.constructed().get(1)).GetDataSize();
|
||||||
|
verifyNoInteractions(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void transientSocketErrorsStayInsideTheBoundedProtocolRetry() throws Exception {
|
||||||
|
var socket = mock(MyUdpSocket.class);
|
||||||
|
var payload = mock(LEDSender2010.class);
|
||||||
|
doThrow(new IOException("transient send")).doNothing().when(socket).send(anyString(), anyInt(), any(), anyInt());
|
||||||
|
when(socket.receive()).thenThrow(new java.io.UncheckedIOException(new IOException("transient receive"))).thenReturn(1);
|
||||||
|
when(payload.parse_trans_respond(any(), anyInt(), anyInt(), anyInt())).thenReturn(1);
|
||||||
|
assertThat(ProtocolDemo.send_data1(socket, payload, "127.0.0.1")).isTrue();
|
||||||
|
verify(socket, times(3)).send(anyString(), anyInt(), any(), anyInt());
|
||||||
|
verify(socket, times(3)).receive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void persistentSendFailureStopsAfterFiveAttempts() throws Exception {
|
||||||
|
var socket = mock(MyUdpSocket.class);
|
||||||
|
doThrow(new IOException("unreachable")).when(socket).send(anyString(), anyInt(), any(), anyInt());
|
||||||
|
assertThat(ProtocolDemo.send_data1(socket, mock(LEDSender2010.class), "127.0.0.1")).isFalse();
|
||||||
|
verify(socket, times(5)).send(anyString(), anyInt(), any(), anyInt());
|
||||||
|
verify(socket, never()).receive();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
ruoyi-admin/src/test/java/com/ruoyi/Dzp/MyUdpSocketTest.java
Normal file
50
ruoyi-admin/src/test/java/com/ruoyi/Dzp/MyUdpSocketTest.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ruoyi.Dzp;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.io.IOException;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
|
class MyUdpSocketTest {
|
||||||
|
@Test
|
||||||
|
void occupiedPortFailsConstructionInsteadOfReturningBrokenSocket() throws Exception {
|
||||||
|
try (var occupied = new DatagramSocket(0)) {
|
||||||
|
assertThatThrownBy(() -> new MyUdpSocket(occupied.getLocalPort()))
|
||||||
|
.isInstanceOf(IOException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void receivedPeerBelongsToItsOwnSocket() throws Exception {
|
||||||
|
MyUdpSocket first = new MyUdpSocket(0);
|
||||||
|
MyUdpSocket second = new MyUdpSocket(0);
|
||||||
|
try (var peerOne = new DatagramSocket(0); var peerTwo = new DatagramSocket(0)) {
|
||||||
|
exchange(first, peerOne, (byte) 11);
|
||||||
|
int firstPeer = first.get_client_port();
|
||||||
|
exchange(second, peerTwo, (byte) 22);
|
||||||
|
assertThat(first.get_client_port()).isEqualTo(firstPeer);
|
||||||
|
assertThat(second.get_client_port()).isEqualTo(peerTwo.getLocalPort());
|
||||||
|
first.close();
|
||||||
|
exchange(second, peerTwo, (byte) 33);
|
||||||
|
} finally {
|
||||||
|
first.close();
|
||||||
|
second.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exchange(MyUdpSocket socket, DatagramSocket peer, byte value) throws Exception {
|
||||||
|
peer.setSoTimeout(1000);
|
||||||
|
socket.send("127.0.0.1", peer.getLocalPort(), new byte[]{value}, 1);
|
||||||
|
var request = new DatagramPacket(new byte[64], 64);
|
||||||
|
peer.receive(request);
|
||||||
|
peer.send(new DatagramPacket(new byte[]{value}, 1, InetAddress.getLoopbackAddress(), request.getPort()));
|
||||||
|
int size = 0;
|
||||||
|
for (int attempt = 0; attempt < 100 && size == 0; attempt++) size = socket.receive();
|
||||||
|
assertThat(size).isEqualTo(1);
|
||||||
|
byte[] result = new byte[1];
|
||||||
|
socket.get_receive_packet(result, 1);
|
||||||
|
assertThat(result).containsExactly(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -172,6 +172,23 @@ class ShadowInboundEligibilityTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multipleProductsWithOneFieldGroupCanContinueTheMatchingProjectLane() {
|
||||||
|
jdbc.update("update alg.shadow_inbound_request_snapshot set detail_count=5,product_scope_count=5,product_id=null,lot_no=null");
|
||||||
|
space("RESIDENT", "LANE", "1", "2", "N", "OLD", "C");
|
||||||
|
stock("RESIDENT", "OLD");
|
||||||
|
assertThat(eligible()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ambiguousFieldGroupsOnlyAllowEntirelyEmptyLanes() {
|
||||||
|
jdbc.update("update alg.shadow_inbound_request_snapshot set detail_count=5,field_group_count=2,product_scope_count=5");
|
||||||
|
assertThat(eligible()).isEqualTo(1);
|
||||||
|
space("RESIDENT", "LANE", "1", "2", "N", "OLD", "C");
|
||||||
|
stock("RESIDENT", "OLD");
|
||||||
|
assertThat(eligible()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
private void space(String id, String lane, String floor, String status, String locked, String tray, String area) {
|
private void space(String id, String lane, String floor, String status, String locked, String tray, String area) {
|
||||||
jdbc.update("insert into alg.cargo_space_snapshot values ('01',42,?,?,?,?,?,?,?,'UNRELATED')",
|
jdbc.update("insert into alg.cargo_space_snapshot values ('01',42,?,?,?,?,?,?,?,'UNRELATED')",
|
||||||
id, lane, floor, status, locked, tray, area);
|
id, lane, floor, status, locked, tray, area);
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class ShadowInboundValidationTest {
|
|||||||
reference.setTrayJobCount(1);
|
reference.setTrayJobCount(1);
|
||||||
reference.setLegacyScopeComplete(1);
|
reference.setLegacyScopeComplete(1);
|
||||||
when(mapper.selectInboundInput("01", 42, "31331", "TRAY-1")).thenReturn(reference);
|
when(mapper.selectInboundInput("01", 42, "31331", "TRAY-1")).thenReturn(reference);
|
||||||
|
when(mapper.selectInboundEligibleSpaces("01", 42, "31331", "TRAY-1")).thenReturn(java.util.List.of("SPACE-9"));
|
||||||
when(mapper.countInboundEligibleSpace("01", 42, "31331", "TRAY-1", "SPACE-9")).thenReturn(1);
|
when(mapper.countInboundEligibleSpace("01", 42, "31331", "TRAY-1", "SPACE-9")).thenReturn(1);
|
||||||
var assignment = new InboundAssignment("HPR-1", "SPACE-9", "LANE", 1, 1,
|
var assignment = new InboundAssignment("HPR-1", "SPACE-9", "LANE", 1, 1,
|
||||||
InboundLadderRung.CONTINUE, InboundBorrowType.NONE, null,
|
InboundLadderRung.CONTINUE, InboundBorrowType.NONE, null,
|
||||||
@@ -87,13 +88,22 @@ class ShadowInboundValidationTest {
|
|||||||
reference.setDetailCount(45);
|
reference.setDetailCount(45);
|
||||||
reference.setFieldGroupCount(2);
|
reference.setFieldGroupCount(2);
|
||||||
reference.setProductScopeCount(45);
|
reference.setProductScopeCount(45);
|
||||||
assertSkipped("SKIPPED_AMBIGUOUS_LEGACY_FIELDS");
|
var result = plan();
|
||||||
|
assertThat(result.resultCode()).isEqualTo("READY_CONSERVATIVE");
|
||||||
|
assertThat(result.reason()).contains("legacyEligibility=EMPTY_LANE_ONLY");
|
||||||
|
engine.compareInbound(result, "10", "SPACE-9");
|
||||||
|
verify(mapper).insertComparison(any(), any(), eq("INBOUND_COMPARE"), anyLong(), any(), any(),
|
||||||
|
eq("MATCH_CONSERVATIVE"), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void multiProductTrayDoesNotSilentlyUseOneRepresentativeProduct() {
|
void multiProductTrayDoesNotSilentlyUseOneRepresentativeProduct() {
|
||||||
reference.setProductScopeCount(2);
|
reference.setProductScopeCount(2);
|
||||||
assertSkipped("SKIPPED_MULTI_PRODUCT_INPUT");
|
assertThat(plan().resultCode()).isEqualTo("READY");
|
||||||
|
verify(facade).planInboundFromPublishedSnapshot(eq("01"), argThat(request ->
|
||||||
|
request.productId() == null && request.lotNo() == null
|
||||||
|
&& request.batchLaneMode() == com.ruoyi.algorithm.inbound.BatchLaneMode.OPEN
|
||||||
|
&& request.eligibleSpaceIds().equals(java.util.Set.of("SPACE-9"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -148,6 +158,12 @@ class ShadowInboundValidationTest {
|
|||||||
|
|
||||||
private InboundShadowObservation plan() { return engine.planInbound(snapshot, "31331", "TRAY-1"); }
|
private InboundShadowObservation plan() { return engine.planInbound(snapshot, "31331", "TRAY-1"); }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noConservativeSpaceDoesNotInventAnUnrestrictedRequest() {
|
||||||
|
when(mapper.selectInboundEligibleSpaces("01", 42, "31331", "TRAY-1")).thenReturn(java.util.List.of());
|
||||||
|
assertSkipped("SKIPPED_NO_CONSERVATIVE_SPACE");
|
||||||
|
}
|
||||||
|
|
||||||
private void assertSkipped(String code) {
|
private void assertSkipped(String code) {
|
||||||
var result = plan();
|
var result = plan();
|
||||||
assertThat(result.resultCode()).isEqualTo(code);
|
assertThat(result.resultCode()).isEqualTo(code);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class ShadowOutboundValidationTest {
|
|||||||
candidate.setTrayJobCount(0);
|
candidate.setTrayJobCount(0);
|
||||||
when(mapper.selectProjectOutboundReference("01", 459L, "DETAIL")).thenReturn(input);
|
when(mapper.selectProjectOutboundReference("01", 459L, "DETAIL")).thenReturn(input);
|
||||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate));
|
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate));
|
||||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any())).thenReturn(
|
when(facade.planShadowOutboundCandidates(eq("01"), any(), any())).thenReturn(
|
||||||
OutboundPlan.ready(new OutboundAssignment("DETAIL", "TRAY", input.getProjectNo(), "PRODUCT",
|
OutboundPlan.ready(new OutboundAssignment("DETAIL", "TRAY", input.getProjectNo(), "PRODUCT",
|
||||||
"SPACE", 1, false, List.of(), List.of(), false, 0, 0, 0,
|
"SPACE", 1, false, List.of(), List.of(), false, 0, 0, 0,
|
||||||
FifoDecision.off(), Optional.empty(), "proposal"), TaskCountEstimate.zero()));
|
FifoDecision.off(), Optional.empty(), "proposal"), TaskCountEstimate.zero()));
|
||||||
@@ -71,7 +71,7 @@ class ShadowOutboundValidationTest {
|
|||||||
assertThat(result.resultCode()).isEqualTo("READY");
|
assertThat(result.resultCode()).isEqualTo("READY");
|
||||||
assertThat(result.proposedStockId()).isEqualTo("STOCK");
|
assertThat(result.proposedStockId()).isEqualTo("STOCK");
|
||||||
assertThat(result.reason()).contains("predicateVersion=1", "requestedQuantity=3", "selectedProject=AD.L-00033.01.01");
|
assertThat(result.reason()).contains("predicateVersion=1", "requestedQuantity=3", "selectedProject=AD.L-00033.01.01");
|
||||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), argThat(r -> !r.wholeTrayOrDefault()));
|
verify(facade).planShadowOutboundCandidates(eq("01"), argThat(r -> !r.wholeTrayOrDefault()), eq(java.util.Set.of("STOCK")));
|
||||||
verify(mapper, never()).refreshSnapshot(anyString(), anyLong(), anyString());
|
verify(mapper, never()).refreshSnapshot(anyString(), anyLong(), anyString());
|
||||||
verify(facade, never()).planOutbound(anyString(), any());
|
verify(facade, never()).planOutbound(anyString(), any());
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ class ShadowOutboundValidationTest {
|
|||||||
void emptyProjectDerivesOnlyProvenCandidateScope() {
|
void emptyProjectDerivesOnlyProvenCandidateScope() {
|
||||||
input.setProjectNo(null);
|
input.setProjectNo(null);
|
||||||
assertThat(plan().resultCode()).isEqualTo("READY");
|
assertThat(plan().resultCode()).isEqualTo("READY");
|
||||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), argThat(r -> "AD.L-00033.01.01".equals(r.projectNo())));
|
verify(facade).planShadowOutboundCandidates(eq("01"), argThat(r -> "AD.L-00033.01.01".equals(r.projectNo())), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -134,9 +134,12 @@ class ShadowOutboundValidationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void multipleRowsOnSelectedTrayAreExplicitlyUnsupported() {
|
void otherRowsOnSelectedTrayDoNotEraseTheEligibleItem() {
|
||||||
candidate.setTrayItemCount(2);
|
candidate.setTrayItemCount(2);
|
||||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_MULTI_STOCK_TRAY");
|
var result = plan();
|
||||||
|
assertThat(result.resultCode()).isEqualTo("READY");
|
||||||
|
assertThat(result.proposedStockId()).isEqualTo("STOCK");
|
||||||
|
assertThat(result.reason()).contains("candidateEvidenceComplete=true", "\"trayItems\":2", "\"stock\":\"STOCK\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -155,4 +158,13 @@ class ShadowOutboundValidationTest {
|
|||||||
private OutboundShadowObservation plan() {
|
private OutboundShadowObservation plan() {
|
||||||
return engine.planProjectOutbound(snapshot, "DETAIL");
|
return engine.planProjectOutbound(snapshot, "DETAIL");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unsupportedDuplicateEligibleRowsRemainExplicitAndKeepEvidence() {
|
||||||
|
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate, candidate));
|
||||||
|
var result = plan();
|
||||||
|
assertThat(result.resultCode()).isEqualTo("SKIPPED_AMBIGUOUS_STOCK_SCOPE");
|
||||||
|
assertThat(result.reason()).contains("candidateCount=2", "candidateSha256=");
|
||||||
|
verifyNoInteractions(facade);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class ShadowPlanningEngineTest {
|
|||||||
ShadowInboundReference input = inboundReference("1");
|
ShadowInboundReference input = inboundReference("1");
|
||||||
input.setShadowCandidateCount(3);
|
input.setShadowCandidateCount(3);
|
||||||
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(input);
|
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(input);
|
||||||
|
when(mapper.selectInboundEligibleSpaces("01", 42L, "TASK-1", "TRAY-1")).thenReturn(java.util.List.of("SPACE-9"));
|
||||||
when(mapper.countInboundEligibleSpace("01", 42L, "TASK-1", "TRAY-1", "SPACE-9")).thenReturn(1);
|
when(mapper.countInboundEligibleSpace("01", 42L, "TASK-1", "TRAY-1", "SPACE-9")).thenReturn(1);
|
||||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||||
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
|
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
|
||||||
@@ -89,7 +90,7 @@ class ShadowPlanningEngineTest {
|
|||||||
.thenReturn(java.util.List.of(stockReference("01")));
|
.thenReturn(java.util.List.of(stockReference("01")));
|
||||||
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
|
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
|
||||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
|
when(facade.planShadowOutboundCandidates(eq("01"), any(), any()))
|
||||||
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
|
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
|
||||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ class ShadowPlanningEngineTest {
|
|||||||
"sourceSignature=ABC123", "detail=DETAIL-1", "rawWarehouse=W02",
|
"sourceSignature=ABC123", "detail=DETAIL-1", "rawWarehouse=W02",
|
||||||
"project=PROJECT-1", "product=PRODUCT-1", "shadowMatches=1");
|
"project=PROJECT-1", "product=PRODUCT-1", "shadowMatches=1");
|
||||||
verify(mapper).selectProjectOutboundReference("01", 42L, "DETAIL-1");
|
verify(mapper).selectProjectOutboundReference("01", 42L, "DETAIL-1");
|
||||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), any());
|
verify(facade).planShadowOutboundCandidates(eq("01"), any(), eq(java.util.Set.of("STOCK-1")));
|
||||||
verify(facade, never()).planOutbound(any(), any());
|
verify(facade, never()).planOutbound(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +360,8 @@ class ShadowPlanningEngineTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ShadowPlanningEngine engine(SchedulingFacade facade, ShadowInputMapper mapper) {
|
private static ShadowPlanningEngine engine(SchedulingFacade facade, ShadowInputMapper mapper) {
|
||||||
|
when(mapper.selectInboundEligibleSpaces(any(), any(Long.class), any(), any()))
|
||||||
|
.thenReturn(java.util.List.of("SPACE-9"));
|
||||||
return new ShadowPlanningEngine(facade, mapper, properties(), new CapturingTransactionManager());
|
return new ShadowPlanningEngine(facade, mapper, properties(), new CapturingTransactionManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ruoyi.in.stockindetail.controller;
|
||||||
|
|
||||||
|
import com.ruoyi.framework.web.exception.GlobalExceptionHandler;
|
||||||
|
import com.ruoyi.in.stockindetail.service.IInStockinDetailService;
|
||||||
|
import com.ruoyi.in.stockindetailfinish.service.IInStockinDetailFinishService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
class InStockinDetailBindingTest {
|
||||||
|
@org.springframework.web.bind.annotation.RestController
|
||||||
|
static class BrokenServer {
|
||||||
|
@org.springframework.web.bind.annotation.GetMapping("/fixture/server-failure")
|
||||||
|
Object get() { throw new org.springframework.beans.InvalidPropertyException(BrokenServer.class,
|
||||||
|
"stockinId[limit]", "server property failure"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serverPropertyFailureIsNotRelabeledAsBadClientInput() throws Exception {
|
||||||
|
MockMvcBuilders.standaloneSetup(new BrokenServer()).setControllerAdvice(new GlobalExceptionHandler()).build()
|
||||||
|
.perform(get("/fixture/server-failure").param("stockinId[limit]", "20"))
|
||||||
|
.andExpect(jsonPath("$.code").value(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedOrderNumberIsRejectedBeforeEitherDatabaseQuery() throws Exception {
|
||||||
|
var controller = new InStockinDetailController();
|
||||||
|
var active = mock(IInStockinDetailService.class);
|
||||||
|
var finished = mock(IInStockinDetailFinishService.class);
|
||||||
|
ReflectionTestUtils.setField(controller, "inStockinDetailService", active);
|
||||||
|
ReflectionTestUtils.setField(controller, "inStockinDetailFinishService", finished);
|
||||||
|
var mvc = MockMvcBuilders.standaloneSetup(controller)
|
||||||
|
.setControllerAdvice(new GlobalExceptionHandler()).build();
|
||||||
|
|
||||||
|
mvc.perform(get("/in/stockInDetail/list").param("stockinId[limit]", "20"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(400))
|
||||||
|
.andExpect(jsonPath("$.msg").value("请求参数格式不正确,单号等普通字段请传单个值,不要传对象或数组"));
|
||||||
|
verifyNoInteractions(active, finished);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.ruoyi.framework.web.exception;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.InvalidPropertyException;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
@@ -90,6 +91,23 @@ public class GlobalExceptionHandler
|
|||||||
return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
|
return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(InvalidPropertyException.class)
|
||||||
|
public AjaxResult handleInvalidPropertyException(InvalidPropertyException e, HttpServletRequest request)
|
||||||
|
{
|
||||||
|
String property = e.getPropertyName();
|
||||||
|
boolean bindingFailure = java.util.Arrays.stream(e.getStackTrace()).anyMatch(frame ->
|
||||||
|
frame.getClassName().equals("org.springframework.web.method.annotation.ModelAttributeMethodProcessor")
|
||||||
|
&& frame.getMethodName().equals("resolveArgument"));
|
||||||
|
if (!bindingFailure || property == null || !property.contains("[")
|
||||||
|
|| !request.getParameterMap().containsKey(property))
|
||||||
|
{
|
||||||
|
return handleRuntimeException(e, request);
|
||||||
|
}
|
||||||
|
log.warn("请求地址'{}',请求参数结构不正确", request.getRequestURI());
|
||||||
|
return AjaxResult.error(HttpStatus.BAD_REQUEST,
|
||||||
|
"请求参数格式不正确,单号等普通字段请传单个值,不要传对象或数组");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拦截未知的运行时异常
|
* 拦截未知的运行时异常
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user