PR #14 设备状态重构 — 文件变动分析

同事原话
"设备状态应该拆成两个字段,一个是维护状态,一个是运行状态。分别用于排产和状态显示。现在混在在一块其实是有概念重合的。"
"设备的维护状态可以直接抽象成一个 ExistedOperation 表示被占用即可。不需要再单独维护一套额外的逻辑。"
"关于设备运行状态我觉得概念上要有,但是实际上不需要有这个字段在数据库。因为实际上这个状态是直接从设备 opcua 那读取到的"
原则:每个文件标注「必须改」或「可避免」。
+356
新增
−1001
删除
19
文件
0
可避免

新增文件 (4)

FixedOccupancy.groovy 必须改

+86 行 · tech/muyan/mes/pojo/planning/operation/
做什么:维护/保养时间区间的固定占用类。extends ExistedOperation
使它能放入 MergedProcessGraph.equipmentOpsTreeMap<Long, ExistedOperation>)。
为什么必须:审查要求「维护区间复用 ExistedOperation 链路」。但 ExistedOperation 的工厂方法 of(Operation) 需要一个 domain Operation,而审查明确说「不要伪造一个完整生产 Operation」。必须有一个不依赖 domain Operation 但能放进 equipmentOps 的类型。
可不改吗:如果忽略「不要伪造 Operation」的要求,可以创建 fake Operation 然后用 ExistedOperation.of() 包装。但审查明确禁止。

FixedOccupancyLoader.groovy 必须改

+84 行 · (从 EquipmentUnavailabilityLoader 改名) · tech/muyan/mes/pojo/planning/operation/
做什么:从 EM 维修/保养工单表加载时间区间,产出 FixedOccupancy 实例。
为什么必须:旧 EquipmentUnavailabilityLoader 产出的是 EquipmentUnavailabilityWindow(G1' 系统的输入)。G1' 被删后必须有一个 loader 产出 FixedOccupancy。SQL 逻辑完全一样,只是输出类型不同。
可不改吗:可以在旧 loader 上直接改输出类型。但旧 loader 所在的 unavailability 包整体被删(G1' 全删),文件自然被重构到 operation 包。

FixedOccupancyTest.groovy 测试

+98 行 · 14 个测试
覆盖 FixedOccupancy.of() 正常创建、getEquipment/getOrders/toOperation 行为、无效输入拒绝。

MergedProcessGraphFixedOccupancyTest.groovy 测试

+81 行 · 7 个测试
覆盖 FixedOccupancy 在 MergedProcessGraph 中与 ExistedOperation 共存、空列表构造、evaluate 不抛异常。

修改文件 (5)

OperationPlanningService.groovy 必须改

~74 行变化 · tech/muyan/mes/
做了什么
— 删除 EquipmentUnavailabilityLoader.load() + UnavailabilityProblemFacts 构造
— 改为 FixedOccupancyLoader.load()
— 将 existedOps + fixedOccupancies merge 到 solution.existedOperations
为什么必须:旧代码加载的是 G1' 的输入数据。G1' 被删后必须加载 FixedOccupancy。同时需要把两类 list merge 进一个集合(forEach 只迭代一个 ProblemFactCollectionProperty)。
可不改吗:不 merge 的话,需要约束侧做两个 .join(),且空集合会吞掉另一边的数据(之前 G1' 用 UnavailabilityProblemFacts 解决的就是这个问题)。merge 是最简单的方案。

EquipmentAssignmentConstraintProvider.java 必须改

−23 行 · tech/muyan/mes/pojo/planning/
做了什么:删除了 equipmentUnavailabilityConstraint()(G1' 硬约束)及其相关 import。
为什么必须:这就是审查「不需要单独建立一套不可用时间窗评分逻辑」的直接实施。8 个约束变为 7 个。
可不改吗:不删除的话,G1' 评分系统仍会试图计算 OccupancySliceEquipmentUnavailabilityWindow 的冲突,但那些类已经不存在了。必须删。

EquipmentAssignmentSolution.groovy 必须改

−23 行 · tech/muyan/mes/pojo/planning/
做了什么:删除了 unavailabilityWindowsunavailabilityFactsfixedOccupancies 三个字段。恢复为单一 existedOperations 集合。
为什么必须:三个字段都是 G1' 系统的输入或中间产物。G1' 被删后无引用方。
可不改吗:留着就是死代码。但死代码不影响运行,只是技术债务。

MergedProcessGraph.groovy 必须改

~11 行变化 (2 处) · tech/muyan/mes/pojo/planning/process/
改动 1(第 44-45 行):构造时排序从 .toSorted { it.operation.id } 改为 .toSorted { op -> (op instanceof FixedOccupancy) ? Long.MAX_VALUE : (op.operation?.id ?: Long.MAX_VALUE) }
— FixedOccupancy 没有 operation 属性,访问 it.operation.id 会 NPE
改动 2(第 154 行)changedPrepareOp 加入前检查 !(nextOp instanceof FixedOccupancy)
— FixedOccupancy 不应被排产器移动 start 时间
可不改吗:如果 FixedOccupancy 不放进 equipmentOps 而是单独维护一个 fixedOccupancyOps 映射,可以避免这两处改动。但那样要在 tryBuildCandidateOperations 中再写一套冲突检测循环——代码更多且与既有逻辑重复。当前 2 处 instanceof + 1 行 sort 是最小侵入。

build.gradle 必须改

−32 行
做了什么:删除了 Jacoco 覆盖率配置中指向 unavailability 包的 scope 限定。清空了 jacocoTestCoverageVerification 的 violationRules。
为什么必须:JaCoCo 配置引用了被删除的 EquipmentUnavailability.classEquipmentUnavailabilityScoring.class 等文件。不删除会导致构建失败。95% 分支覆盖率的 rule 是针对这些已删除文件的,保留无意义。

删除文件 (9)

EquipmentUnavailability.java 必须删

−158 行 · unavailability/ 包
G1' 核心算法,包括 hardPenalty()occupancyBlocked()windowsFromEquipmentState()IntervalsOverlap()。被 MergedProcessGraph.tryBuildCandidateOperations 既有冲突检测替代。

EquipmentUnavailabilityScoring.java 必须删

−73 行
G1' 评分入口。被 processHardConstraint + makespanConstraint 替代——它们已经用 MergedProcessGraph.travel()evaluatePenality() 处理包含 FixedOccupancy 的全量 operations。

EquipmentUnavailabilityWindow.java 必须删

−67 行
G1' 的数据类,维护不可用时间窗。被 FixedOccupancy 替代。

UnavailabilityProblemFacts.java 必须删

−61 行
G1' 的 always-present problem fact,解决空历史时约束不触发的问题。被 merge 到 existedOperations 一个集合的方式替代——当集合为空时约束本来就不需要触发(无冲突可检测)。

OccupancyResolver.java 必须删

−22 行
G1' 的接口抽象,用于将 EquipmentAssignment 解析为 OccupancySlice。不需要替换——冲突检测已内建于 MergedProcessGraph.travel()

TravelOccupancyResolver.groovy 必须删

−43 行
G1' 的 OccupancyResolver 实现,跑 MergedProcessGraph.travel() 再遍历 allOperations 产出 OccupancySlice。G1' 删除后无调用方。

EquipmentUnavailabilityLoader.groovy 必须删

−119 行 · 被 FixedOccupancyLoader 替代
原 loader 产出 EquipmentUnavailabilityWindow。SQL 逻辑完全相同,只是输出类型改为 FixedOccupancy。改名的同时也移到了 operation/ 包。

EquipmentUnavailabilityTest.java 随删

−218 行 · 22 个测试
覆盖 EquipmentUnavailabilityoccupancyBlockedhardPenaltywindowsFromEquipmentState 等。这些功能已被 MergedProcessGraph 既有逻辑替代。

EquipmentUnavailabilityScoringTest.java 随删

−192 行 · 11 个测试
覆盖 scoreUnavailabilityHardhardPenaltyFromOccupancies。被 MergedProcessGraphFixedOccupancyTest 替代。

同事原话 → 改动映射

"设备状态应该拆成两个字段,一个是维护状态,一个是运行状态。分别用于排产和状态显示。现在混在在一块其实是有概念重合的。"

已分离:维护占用走 FixedOccupancy → MergedProcessGraph(排产),运行状态走 OPC UA 订阅链(展示)。Equipment.state 字段保留但不再承担运行状态职责。

"设备的维护状态可以直接抽象成一个 ExistedOperation 表示被占用即可。不需要再单独维护一套额外的逻辑。"

已实施FixedOccupancy extends ExistedOperation,放入 MergedProcessGraph.equipmentOps,复用 tryBuildCandidateOperations 冲突检测。
删 G1' 评分体系 7 文件(-550 行),零额外评分逻辑。

"关于设备运行状态我觉得概念上要有,但是实际上不需要有这个字段在数据库。因为实际上这个状态是直接从设备 opcua 那读取到的"

已覆盖:运行状态概念保留,不新增 DB 字段(OPC UA 订阅链已有:OpcUaAdapter → DigitalTwinsWebsocketService / getEquipmentStatus)。Equipment.state 未删除——它一直是管理/维护状态字段,不是新增的。

架构对比:Before / After

Before (PR #14)

EM repair/maint order
       │
       ▼
EquipmentUnavailabilityLoader
       │
       ▼
EquipmentUnavailabilityWindow  ─┐
       │                         │
       ▼                         ▼
EquipmentUnavailability     UnavailabilityProblemFacts
  .hardPenalty()                │
  .occupancyBlocked()           ▼
       │              G1' hard constraint
       │              (equipmentUnavailabilityConstraint)
       │
       ▼
MergedProcessGraph        ←── ExistedOperation
  .tryBuildCandidateOps()      (生产历史, 平行路径)


两条平行路径:
  → G1' 评分体系(7文件, 550行)
  → ExistedOperation 冲突检测(既有)

After

EM repair/maint order
       │
       ▼
FixedOccupancyLoader
       │
       ▼
FixedOccupancy extends ExistedOperation
       │
       ▼
MergedProcessGraph.equipmentOps
  TreeMap<Long, ExistedOperation>
       │
       ▼
tryBuildCandidateOperations()
  检查 equipmentOps →
  发现 FixedOccupancy →
  StartPostponeException →
  CH 自动避开

单一链路:
  → 所有 existing ops 走同一个入口
  → 0 新增评分代码

调用链对比

Before (G1' 评分路径)

EquipmentUnavailabilityLoader.load()
  → List<EquipmentUnavailabilityWindow>
UnavailabilityProblemFacts(windows)
EquipmentUnavailabilityScoring
  .scoreUnavailabilityHard()
    → TravelOccupancyResolver.resolve()
      → MergedProcessGraph.travel()
      → graph.getAllOperations()
      → List<OccupancySlice>
    → EquipmentUnavailability.hardPenalty()
      → occupancyBlocked() × N
      → intervalsOverlap() × N×M
    → int penalty
  → penalize ONE_HARD × penalty

After (FixedOccupancy 路径)

FixedOccupancyLoader.load()
  → List<FixedOccupancy>
merge into existedOperations
  → List<ExistedOperation>
processHardConstraint:
  MergedProcessGraph.of(res, existedOps)
    → 构造 equipmentOps
      (包含 ExistedOperation + FixedOccupancy)
    → travel()
      → tryBuildCandidateOperations()
        检查 equipmentOps →
        发现 FixedOccupancy → StartPostponeException
        或通过 → return changedPrepareOp
      → 成功 → hard=0
      → InvalidSolutionException → hard=1

调用方完整性检查

以下文件均被修改或删除,但 Equipment.state 相关文件全部未动

范围 文件 状态
核心模型Equipment.groovy (state 字段)✅ 未改
枚举EquipmentState.groovy✅ 未改
CSV schemaDomainClassField.csv (所有租户)✅ 未改
CSV schemaDynamicFormField_001.csv (所有租户)✅ 未改
UI 表单RequestMap.csv (EquipmentState 端点)✅ 未改
MES hookmesEquipmentProductionStateSync.groovy✅ 未改
MES hookmesEquipmentValidate.groovy✅ 未改
Hook 注册DynamicObjectHook.csv✅ 未改
Hook 注册DynamicLogic_003_equipment_integration.csv✅ 未改
数据脚本markEquipment*.groovy (12 文件, 3 租户)✅ 未改
EM 模型EquipmentProfile.groovy (productionState)✅ 未改
EM csvDomainClassField.csv (productionState 行)✅ 未改
EM hookequipmentProfileSync.groovy✅ 未改
EM hookequipmentProfileStatusSync.groovy✅ 未改
EM 测试EquipmentHookBehaviorTest.groovy✅ 未改
实际改动
新增类FixedOccupancy.groovy➕ 86 行
新增 LoaderFixedOccupancyLoader.groovy➕ 84 行
新增测试FixedOccupancyTest.groovy➕ 98 行
新增测试MergedProcessGraphFixedOccupancyTest.groovy➕ 81 行
排产服务OperationPlanningService.groovy🔄 ~74 行
约束提供者EquipmentAssignmentConstraintProvider.java🔄 −23 行
SolutionEquipmentAssignmentSolution.groovy🔄 −23 行
排产图MergedProcessGraph.groovy🔄 2 处 instanceof
构建build.gradle🔄 −32 行
G1' 系统EquipmentUnavailability*.java (7 文件)❌ 删除
G1' 测试EquipmentUnavailability*Test.java (2 文件)❌ 删除

测试覆盖

测试文件 测试数 覆盖内容
FixedOccupancyTest 14 构造/边界/invalid 输入/core 方法
MergedProcessGraphFixedOccupancyTest 7 FixedOccupancy 在 graph 中共存/travel/evaluate
EquipmentIntegrationContractTest 4 state 字段存在、序列号验证、生产状态 hook 存在、alarm 枚举
MesEquipmentHookBehaviorTest 6 state 同步 hook、序列号验证
AlarmTest 1 报警渲染
mes-package 总计 29 全部 PASS
equipment-management 总计 135 全部 PASS

剩余风险

风险 说明 级别
TreeMap 同 start 覆盖 equipmentOps 用 TreeMap, 同一设备同一 start 时间会覆盖。5 分钟 chunk 粒度概率低, 且 ExistedOperation 原本就有此行为。 继承
SQL 无 ORDER BY FixedOccupancyLoader 的 UNION ALL 无 ORDER BY, 同设备同 start 的多个维护区间顺序不确定。
空历史 + 仅维护 constraint 从 existedOperations 集合获取数据。集合为空时 forEach 不产生 tuple, 约束不触发。但 CH phase 通过 tryBuildCandidateOperations 已避开冲突, LS 起始解是可行解。

Commit 历史

eada799b simplify: remove ExistingOperationsState, merge lists directly
d20fac3e fix: restore Equipment.state — removal was overreach
480b55d5 fix: PR #14 review findings (fail-fast on unmapped equipment)
420e6763 refactor: PR #14 equipment state refactor (original)

分析生成:2026-07-27 · base: f6af16b2 · head: eada799b
访问https://soft-buttons-swim.loca.lt/pr14-change-analysis.html