From d9b335c6c9bde9581f4e012eec6e3cd6f2c26ab1 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 8 Jan 2026 15:14:08 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E3=80=90ops=E3=80=91=E6=B4=BE?= =?UTF-8?q?=E5=8D=95=E7=AD=96=E7=95=A5=E9=80=BB=E8=BE=91=E7=BC=96=E5=86=99?= =?UTF-8?q?=EF=BC=88=E6=9A=82=E6=97=B6=E9=A2=84=E7=95=99=E3=80=81=E5=90=8E?= =?UTF-8?q?=E6=9C=9F=E8=AE=BE=E8=AE=A1=E6=8E=A5=E5=85=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/dispatch/AssigneeRecommendation.java | 62 +++++ .../ops/core/dispatch/DispatchContext.java | 127 +++++++++ .../ops/core/dispatch/DispatchEngine.java | 80 ++++++ .../ops/core/dispatch/DispatchEngineImpl.java | 251 ++++++++++++++++++ .../ops/core/dispatch/DispatchStrategy.java | 85 ++++++ .../ops/core/dispatch/InterruptDecision.java | 69 +++++ 6 files changed, 674 insertions(+) create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/AssigneeRecommendation.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchContext.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngine.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchStrategy.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/InterruptDecision.java diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/AssigneeRecommendation.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/AssigneeRecommendation.java new file mode 100644 index 0000000..983b90d --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/AssigneeRecommendation.java @@ -0,0 +1,62 @@ +package com.viewsh.module.ops.core.dispatch; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 派单推荐结果 + * 包含推荐的执行人员及推荐理由 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AssigneeRecommendation { + + /** + * 推荐的执行人员ID + */ + private Long assigneeId; + + /** + * 执行人员姓名 + */ + private String assigneeName; + + /** + * 匹配分数(0-100) + * 分数越高表示越匹配 + */ + private Integer score; + + /** + * 推荐理由 + * 例如:"同区域、电量充足、当前空闲" + */ + private String reason; + + /** + * 创建空推荐结果(表示没有合适的人员) + */ + public static AssigneeRecommendation none() { + return new AssigneeRecommendation(null, null, 0, "无可用人员"); + } + + /** + * 创建推荐结果 + */ + public static AssigneeRecommendation of(Long assigneeId, String assigneeName, Integer score, String reason) { + return new AssigneeRecommendation(assigneeId, assigneeName, score, reason); + } + + /** + * 是否有推荐结果 + */ + public boolean hasRecommendation() { + return assigneeId != null; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchContext.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchContext.java new file mode 100644 index 0000000..67cbb9c --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchContext.java @@ -0,0 +1,127 @@ +package com.viewsh.module.ops.core.dispatch; + +import com.viewsh.module.ops.enums.PriorityEnum; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; +import java.util.Set; + +/** + * 派单上下文 + * 封装派单决策所需的所有信息 + * + * @author lzh + */ +@Data +@Builder +public class DispatchContext { + + /** + * 业务类型(CLEAN、REPAIR、SECURITY等) + */ + private String businessType; + + /** + * 工单ID + */ + private Long orderId; + + /** + * 区域ID + */ + private Long areaId; + + /** + * 优先级 + */ + private PriorityEnum priority; + + /** + * 技能要求(可选) + * Key: 技能类型, Value: 技能等级 + */ + private Map skillRequirements; + + /** + * 排除的执行人员ID列表(可选) + * 例如:已分配过该工单的人员 + */ + private Set excludedAssigneeIds; + + /** + * 首选执行人员ID(可选) + * 例如:指定的保洁员 + */ + private Long preferredAssigneeId; + + /** + * 扩展参数(可选) + * 用于传递业务特定的参数 + */ + private Map extraParams; + + /** + * 创建保洁派单上下文 + */ + public static DispatchContext forCleaner(Long orderId, Long areaId, PriorityEnum priority) { + return DispatchContext.builder() + .businessType("CLEAN") + .orderId(orderId) + .areaId(areaId) + .priority(priority) + .build(); + } + + /** + * 创建安保派单上下文 + */ + public static DispatchContext forSecurity(Long orderId, Long areaId, PriorityEnum priority) { + return DispatchContext.builder() + .businessType("SECURITY") + .orderId(orderId) + .areaId(areaId) + .priority(priority) + .build(); + } + + /** + * 创建维修派单上下文 + */ + public static DispatchContext forRepair(Long orderId, Long areaId, PriorityEnum priority) { + return DispatchContext.builder() + .businessType("REPAIR") + .orderId(orderId) + .areaId(areaId) + .priority(priority) + .build(); + } + + /** + * 获取扩展参数 + */ + @SuppressWarnings("unchecked") + public T getExtraParam(String key, Class type) { + if (extraParams == null) { + return null; + } + Object value = extraParams.get(key); + if (value == null) { + return null; + } + if (type.isInstance(value)) { + return (T) value; + } + return null; + } + + /** + * 添加扩展参数 + */ + public void addExtraParam(String key, Object value) { + if (this.extraParams == null) { + this.extraParams = new java.util.HashMap<>(); + } + this.extraParams.put(key, value); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngine.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngine.java new file mode 100644 index 0000000..2ca3fed --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngine.java @@ -0,0 +1,80 @@ +package com.viewsh.module.ops.core.dispatch; + +import java.util.List; + +/** + * 派单引擎 - 纯决策层 + *

+ * 职责: + * 1. 根据策略推荐最合适的执行人员 + * 2. 不涉及状态管理(队列状态、工单状态由各自服务管理) + * 3. 不涉及设备通知(由通知服务处理) + *

+ * 设计原则: + * - 单一职责:只负责派单决策 + * - 开闭原则:通过策略模式支持扩展 + * - 依赖倒置:业务层依赖接口而非实现 + * + * @author lzh + */ +public interface DispatchEngine { + + /** + * 推荐执行人员(核心方法) + *

+ * 根据派单上下文(区域、优先级、技能要求等)推荐最合适的执行人员 + * + * @param context 派单上下文 + * @return 推荐结果,如果没有合适的返回 AssigneeRecommendation.none() + */ + AssigneeRecommendation recommendAssignee(DispatchContext context); + + /** + * 批量推荐执行人员 + *

+ * 用于场景:需要从多个候选人中选择最优人员,或需要备用人员 + * + * @param context 派单上下文 + * @param limit 返回结果数量限制 + * @return 推荐结果列表,按匹配分数降序排序 + */ + List recommendAssignees(DispatchContext context, int limit); + + /** + * 评估是否可以打断当前任务 + *

+ * 用于P0紧急任务插队场景:判断是否可以打断当前执行人员正在执行的任务 + * + * @param currentAssigneeId 当前执行任务的执行人员ID + * @param urgentContext 紧急任务的派单上下文 + * @return 打断决策结果 + */ + InterruptDecision evaluateInterrupt(Long currentAssigneeId, DispatchContext urgentContext); + + /** + * 注册派单策略 + *

+ * 各业务线(保洁、安保、维修等)实现自己的派单策略,注册到引擎中 + * + * @param strategy 派单策略实现 + */ + void registerStrategy(DispatchStrategy strategy); + + /** + * 注册业务类型与策略的映射 + *

+ * 建立业务类型(CLEAN/REPAIR/SECURITY)与策略名称的映射关系 + * + * @param businessType 业务类型 + * @param strategyName 策略名称 + */ + void registerBusinessTypeStrategy(String businessType, String strategyName); + + /** + * 根据业务类型获取策略 + * + * @param businessType 业务类型 + * @return 派单策略,如果没有找到返回null + */ + DispatchStrategy getStrategyByBusinessType(String businessType); +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java new file mode 100644 index 0000000..6254ccd --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java @@ -0,0 +1,251 @@ +package com.viewsh.module.ops.core.dispatch; + +import com.viewsh.module.ops.enums.PriorityEnum; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 派单引擎实现 + *

+ * 职责: + * 1. 管理派单策略的注册和查找 + * 2. 根据业务类型路由到对应的策略 + * 3. 提供统一的派单决策接口 + *

+ * 注意: + * - 这是纯决策层,不涉及任何状态管理 + * - 队列状态、工单状态由各自服务管理 + * - 设备通知由通知服务处理 + * + * @author lzh + */ +@Slf4j +@Service +public class DispatchEngineImpl implements DispatchEngine { + + /** + * 派单策略注册表 + * Key: 策略名称 + * Value: 策略实现 + */ + private final Map strategyRegistry = new ConcurrentHashMap<>(); + + /** + * 业务类型与策略的映射 + * Key: 业务类型(CLEAN、REPAIR、SECURITY) + * Value: 策略名称 + */ + private final Map businessTypeStrategyMap = new ConcurrentHashMap<>(); + + @PostConstruct + public void init() { + log.info("派单引擎已初始化,等待策略注册..."); + } + + // ========== 策略管理 ========== + + @Override + public void registerStrategy(DispatchStrategy strategy) { + if (strategy == null) { + log.warn("尝试注册空策略,已忽略"); + return; + } + + String strategyName = strategy.getName(); + String businessType = strategy.getSupportedBusinessType(); + + strategyRegistry.put(strategyName, strategy); + + // 自动注册业务类型映射 + if (businessType != null && !businessType.isEmpty()) { + businessTypeStrategyMap.put(businessType, strategyName); + } + + log.info("派单策略已注册: strategyName={}, businessType={}", + strategyName, businessType); + } + + @Override + public void registerBusinessTypeStrategy(String businessType, String strategyName) { + if (businessType == null || businessType.isEmpty()) { + log.warn("业务类型为空,忽略注册"); + return; + } + + if (!strategyRegistry.containsKey(strategyName)) { + log.warn("策略不存在,无法注册映射: businessType={}, strategyName={}", + businessType, strategyName); + return; + } + + businessTypeStrategyMap.put(businessType, strategyName); + log.info("业务类型策略映射已注册: businessType={}, strategyName={}", + businessType, strategyName); + } + + @Override + public DispatchStrategy getStrategyByBusinessType(String businessType) { + if (businessType == null || businessType.isEmpty()) { + return null; + } + + String strategyName = businessTypeStrategyMap.get(businessType); + if (strategyName == null) { + log.debug("未找到业务类型对应的策略: businessType={}", businessType); + return null; + } + + DispatchStrategy strategy = strategyRegistry.get(strategyName); + if (strategy == null) { + log.warn("策略不存在: strategyName={}", strategyName); + } + + return strategy; + } + + // ========== 派单决策方法 ========== + + @Override + public AssigneeRecommendation recommendAssignee(DispatchContext context) { + if (context == null) { + log.warn("派单上下文为空,无法推荐人员"); + return AssigneeRecommendation.none(); + } + + String businessType = context.getBusinessType(); + DispatchStrategy strategy = getStrategyByBusinessType(businessType); + + if (strategy == null) { + log.warn("未找到业务类型对应的派单策略: businessType={}, orderId={}", + businessType, context.getOrderId()); + return AssigneeRecommendation.none(); + } + + try { + AssigneeRecommendation recommendation = strategy.recommendAssignee(context); + if (recommendation != null && recommendation.hasRecommendation()) { + log.info("派单推荐成功: orderId={}, businessType={}, assigneeId={}, score={}, reason={}", + context.getOrderId(), businessType, + recommendation.getAssigneeId(), + recommendation.getScore(), + recommendation.getReason()); + } else { + log.info("派单推荐无合适人员: orderId={}, businessType={}", + context.getOrderId(), businessType); + } + return recommendation; + } catch (Exception e) { + log.error("派单推荐异常: orderId={}, businessType={}", + context.getOrderId(), businessType, e); + return AssigneeRecommendation.none(); + } + } + + @Override + public List recommendAssignees(DispatchContext context, int limit) { + if (context == null) { + log.warn("派单上下文为空,无法推荐人员"); + return Collections.emptyList(); + } + + String businessType = context.getBusinessType(); + DispatchStrategy strategy = getStrategyByBusinessType(businessType); + + if (strategy == null) { + log.warn("未找到业务类型对应的派单策略: businessType={}, orderId={}", + businessType, context.getOrderId()); + return Collections.emptyList(); + } + + try { + List recommendations = strategy.recommendAssignees(context, limit); + log.info("批量派单推荐完成: orderId={}, businessType={}, count={}", + context.getOrderId(), businessType, + recommendations != null ? recommendations.size() : 0); + return recommendations != null ? recommendations : Collections.emptyList(); + } catch (Exception e) { + log.error("批量派单推荐异常: orderId={}, businessType={}", + context.getOrderId(), businessType, e); + return Collections.emptyList(); + } + } + + @Override + public InterruptDecision evaluateInterrupt(Long currentAssigneeId, DispatchContext urgentContext) { + if (currentAssigneeId == null) { + log.warn("当前执行人员ID为空,无法评估打断"); + return InterruptDecision.deny("当前执行人员ID为空", "请检查参数"); + } + + if (urgentContext == null) { + log.warn("紧急任务上下文为空,无法评估打断"); + return InterruptDecision.deny("紧急任务上下文为空", "请检查参数"); + } + + String businessType = urgentContext.getBusinessType(); + DispatchStrategy strategy = getStrategyByBusinessType(businessType); + + if (strategy == null) { + // 使用默认打断规则 + InterruptDecision decision = defaultInterruptDecision(urgentContext); + log.info("使用默认打断规则: currentAssigneeId={}, urgentOrderId={}, canInterrupt={}", + currentAssigneeId, urgentContext.getOrderId(), decision.canInterrupt()); + return decision; + } + + try { + InterruptDecision decision = strategy.evaluateInterrupt( + currentAssigneeId, + null, // currentOrderId 可选 + urgentContext + ); + + log.info("打断评估完成: currentAssigneeId={}, urgentOrderId={}, canInterrupt={}, reason={}", + currentAssigneeId, urgentContext.getOrderId(), + decision.canInterrupt(), decision.getReason()); + + return decision; + } catch (Exception e) { + log.error("打断评估异常: currentAssigneeId={}, urgentOrderId={}", + currentAssigneeId, urgentContext.getOrderId(), e); + return InterruptDecision.deny("评估异常", "使用默认处理"); + } + } + + /** + * 默认打断决策 + * P0任务可以打断任何非P0任务 + */ + private InterruptDecision defaultInterruptDecision(DispatchContext urgentContext) { + if (urgentContext.getPriority() != null && urgentContext.getPriority().isUrgent()) { + return InterruptDecision.allowByDefault(); + } + return InterruptDecision.deny( + "紧急任务优先级不足", + "建议等待当前任务完成" + ); + } + + // ========== 查询方法 ========== + + /** + * 获取所有已注册的策略 + */ + public List getAllStrategies() { + return new ArrayList<>(strategyRegistry.values()); + } + + /** + * 获取所有业务类型与策略的映射 + */ + public Map getAllBusinessTypeMappings() { + return new java.util.HashMap<>(businessTypeStrategyMap); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchStrategy.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchStrategy.java new file mode 100644 index 0000000..f6a878f --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchStrategy.java @@ -0,0 +1,85 @@ +package com.viewsh.module.ops.core.dispatch; + +import java.util.List; + +/** + * 派单策略接口 + *

+ * 各业务模块(保洁、安保、工程等)需要实现此接口,定义自己的派单逻辑 + *

+ * 职责: + * 1. 根据派单上下文推荐合适的执行人员 + * 2. 判断是否可以打断当前任务 + * + * @author lzh + */ +public interface DispatchStrategy { + + /** + * 策略名称 + *

+ * 如:cleaner_area_priority, security_skill_match + * 命名规范:{业务类型}_{策略描述} + * + * @return 策略名称 + */ + String getName(); + + /** + * 支持的业务类型 + *

+ * 如:CLEAN、REPAIR、SECURITY + * + * @return 业务类型 + */ + String getSupportedBusinessType(); + + /** + * 执行派单策略,推荐执行人员 + *

+ * 根据派单上下文(区域、优先级、技能要求等)推荐最合适的执行人员 + * + * @param context 派单上下文 + * @return 推荐结果,如果没有合适的返回 AssigneeRecommendation.none() + */ + AssigneeRecommendation recommendAssignee(DispatchContext context); + + /** + * 批量推荐执行人员 + *

+ * 用于场景:需要从多个候选人中选择,或需要备用人员 + * + * @param context 派单上下文 + * @param limit 返回结果数量限制 + * @return 推荐结果列表,按匹配分数降序排序 + */ + List recommendAssignees(DispatchContext context, int limit); + + /** + * 评估是否可以打断当前任务 + *

+ * 当P0紧急任务需要插队时,判断是否可以打断当前执行的任务 + * + * @param currentAssigneeId 当前执行任务的执行人员ID + * @param currentOrderId 当前正在执行的工单ID(可选) + * @param urgentContext 紧急任务的派单上下文 + * @return 打断决策结果 + */ + InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId, DispatchContext urgentContext); + + /** + * 默认实现:判断是否可以打断 + *

+ * 默认规则:P0任务可以打断任何非P0任务 + */ + default InterruptDecision defaultEvaluateInterrupt(Long currentAssigneeId, Long currentOrderId, + DispatchContext urgentContext) { + if (urgentContext.getPriority() != null && urgentContext.getPriority().isUrgent()) { + return InterruptDecision.allowByDefault(); + } + return InterruptDecision.deny( + "紧急任务优先级不足", + "建议等待当前任务完成" + ); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/InterruptDecision.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/InterruptDecision.java new file mode 100644 index 0000000..9291d6d --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/InterruptDecision.java @@ -0,0 +1,69 @@ +package com.viewsh.module.ops.core.dispatch; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 打断决策结果 + * 用于判断P0紧急任务是否可以打断当前正在执行的任务 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InterruptDecision { + + /** + * 是否可以打断 + */ + @Builder.Default + private boolean canInterrupt = false; + + /** + * 打断原因 + * 例如:"紧急任务优先级更高" + */ + private String reason; + + /** + * 建议操作 + * 例如:"暂停当前任务"、"等待当前任务完成" + */ + private String suggestion; + + /** + * 可以打断 + */ + public static InterruptDecision allow(String reason, String suggestion) { + return new InterruptDecision(true, reason, suggestion); + } + + /** + * 不可以打断 + */ + public static InterruptDecision deny(String reason, String suggestion) { + return new InterruptDecision(false, reason, suggestion); + } + + /** + * 默认可以打断(P0任务) + */ + public static InterruptDecision allowByDefault() { + return InterruptDecision.allow( + "P0紧急任务优先级最高", + "建议暂停当前任务,立即执行P0任务" + ); + } + + /** + * 别名方法,用于更流畅的调用 + * Lombok 会生成 isCanInterrupt(),这里提供 canInterrupt() 别名 + */ + public boolean canInterrupt() { + return isCanInterrupt(); + } +}