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 deleted file mode 100644 index 983b90d8..00000000 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/AssigneeRecommendation.java +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 67cbb9c6..00000000 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchContext.java +++ /dev/null @@ -1,127 +0,0 @@ -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 index 2ca3fed4..a157a8d3 100644 --- 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 @@ -1,80 +1,131 @@ package com.viewsh.module.ops.core.dispatch; +import com.viewsh.module.ops.core.dispatch.model.AssigneeRecommendation; +import com.viewsh.module.ops.core.dispatch.model.DispatchDecision; +import com.viewsh.module.ops.core.dispatch.model.DispatchPath; +import com.viewsh.module.ops.core.dispatch.model.DispatchResult; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; +import com.viewsh.module.ops.core.dispatch.strategy.AssignStrategy; +import com.viewsh.module.ops.core.dispatch.strategy.InterruptDecision; +import com.viewsh.module.ops.core.dispatch.strategy.ScheduleStrategy; +import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult; + import java.util.List; /** - * 派单引擎 - 纯决策层 + * 调度引擎 - 统一派单入口 *

* 职责: - * 1. 根据策略推荐最合适的执行人员 - * 2. 不涉及状态管理(队列状态、工单状态由各自服务管理) - * 3. 不涉及设备通知(由通知服务处理) + * 1. 分配决策:谁来接单(通过 AssignStrategy) + * 2. 调度决策:怎么派单(通过 ScheduleStrategy) + * 3. 执行编排:协调生命周期管理器完成派单 *

* 设计原则: - * - 单一职责:只负责派单决策 - * - 开闭原则:通过策略模式支持扩展 - * - 依赖倒置:业务层依赖接口而非实现 + * - 纯通用逻辑:不包含任何业务特定逻辑(如保洁员状态) + * - 事件驱动:业务特定状态通过事件监听实现 + * - 可扩展:通过策略模式支持不同业务线 * * @author lzh */ public interface DispatchEngine { - /** - * 推荐执行人员(核心方法) - *

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

- * 用于场景:需要从多个候选人中选择最优人员,或需要备用人员 + * 完整的调度流程: + *

    + *
  1. 分配决策:通过 AssignStrategy 推荐执行人
  2. + *
  3. 调度决策:通过 ScheduleStrategy 决定调度路径
  4. + *
  5. 执行编排:协调生命周期管理器完成状态转换
  6. + *
* - * @param context 派单上下文 - * @param limit 返回结果数量限制 - * @return 推荐结果列表,按匹配分数降序排序 + * @param context 调度上下文 + * @return 调度结果 */ - List recommendAssignees(DispatchContext context, int limit); + DispatchResult dispatch(OrderDispatchContext context); /** - * 评估是否可以打断当前任务 + * P0紧急任务插队 *

- * 用于P0紧急任务插队场景:判断是否可以打断当前执行人员正在执行的任务 + * 当P0紧急任务需要派单时: + *

    + *
  1. 分配决策:推荐执行人
  2. + *
  3. 评估打断:判断是否可以打断当前任务
  4. + *
  5. 执行编排:打断当前任务 + 派发紧急任务
  6. + *
* - * @param currentAssigneeId 当前执行任务的执行人员ID - * @param urgentContext 紧急任务的派单上下文 - * @return 打断决策结果 + * @param urgentOrderId 紧急工单ID + * @param assigneeId 执行人ID + * @return 调度结果 */ - InterruptDecision evaluateInterrupt(Long currentAssigneeId, DispatchContext urgentContext); + DispatchResult urgentInterrupt(Long urgentOrderId, Long assigneeId); /** - * 注册派单策略 + * 任务完成后自动调度下一个 *

- * 各业务线(保洁、安保、维修等)实现自己的派单策略,注册到引擎中 + * 优先级: + *

    + *
  1. 检查是否有被中断的任务,优先恢复
  2. + *
  3. 如果没有中断任务,推送队列中的下一个
  4. + *
  5. 如果没有等待任务,通知业务层更新执行人状态
  6. + *
* - * @param strategy 派单策略实现 + * @param completedOrderId 已完成的工单ID + * @param assigneeId 执行人ID + * @return 调度结果 */ - void registerStrategy(DispatchStrategy strategy); + DispatchResult autoDispatchNext(Long completedOrderId, Long assigneeId); + + // ==================== 策略注册 ==================== /** - * 注册业务类型与策略的映射 - *

- * 建立业务类型(CLEAN/REPAIR/SECURITY)与策略名称的映射关系 + * 注册分配策略 * - * @param businessType 业务类型 - * @param strategyName 策略名称 + * @param strategy 分配策略实现 */ - void registerBusinessTypeStrategy(String businessType, String strategyName); + void registerAssignStrategy(String businessType, AssignStrategy strategy); /** - * 根据业务类型获取策略 + * 注册调度策略 * - * @param businessType 业务类型 - * @return 派单策略,如果没有找到返回null + * @param strategy 调度策略实现 */ - DispatchStrategy getStrategyByBusinessType(String businessType); + void registerScheduleStrategy(String businessType, ScheduleStrategy strategy); + + // ==================== 决策方法(供外部调用) ==================== + + /** + * 推荐执行人 + *

+ * 纯决策方法,不涉及状态变更 + * + * @param context 调度上下文 + * @return 推荐结果 + */ + AssigneeRecommendation recommendAssignee(OrderDispatchContext context); + + /** + * 决策调度路径 + *

+ * 纯决策方法,不涉及状态变更 + * + * @param context 调度上下文 + * @return 调度决策 + */ + DispatchDecision decideSchedulePath(OrderDispatchContext context); + + /** + * 评估是否可以打断 + *

+ * 纯决策方法,不涉及状态变更 + * + * @param currentAssigneeId 当前执行人ID + * @param currentOrderId 当前工单ID + * @param urgentContext 紧急任务上下文 + * @return 打断决策 + */ + InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId, + OrderDispatchContext urgentContext); } 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 index 6254ccdd..acdf652d 100644 --- 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 @@ -1,28 +1,43 @@ package com.viewsh.module.ops.core.dispatch; -import com.viewsh.module.ops.enums.PriorityEnum; +import com.viewsh.module.ops.api.queue.OrderQueueDTO; +import com.viewsh.module.ops.api.queue.OrderQueueService; +import com.viewsh.module.ops.core.dispatch.model.*; +import com.viewsh.module.ops.core.dispatch.strategy.AssignStrategy; +import com.viewsh.module.ops.core.dispatch.strategy.InterruptDecision; +import com.viewsh.module.ops.core.dispatch.strategy.ScheduleStrategy; +import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; +import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest; +import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.OperatorTypeEnum; +import com.viewsh.module.ops.enums.OrderQueueStatusEnum; +import com.viewsh.module.ops.enums.WorkOrderStatusEnum; +import com.viewsh.module.ops.infrastructure.log.annotation.BusinessLog; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogType; import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * 派单引擎实现 + * 调度引擎实现 *

* 职责: - * 1. 管理派单策略的注册和查找 - * 2. 根据业务类型路由到对应的策略 - * 3. 提供统一的派单决策接口 + * 1. 管理策略的注册和查找 + * 2. 执行调度流程:分配决策 → 调度决策 → 执行编排 + * 3. 协调生命周期管理器完成状态转换 *

- * 注意: - * - 这是纯决策层,不涉及任何状态管理 - * - 队列状态、工单状态由各自服务管理 - * - 设备通知由通知服务处理 + * 设计原则: + * - 纯通用逻辑:不包含任何业务特定逻辑 + * - 事件驱动:业务特定状态通过事件监听实现 * * @author lzh */ @@ -30,191 +45,295 @@ import java.util.concurrent.ConcurrentHashMap; @Service public class DispatchEngineImpl implements DispatchEngine { - /** - * 派单策略注册表 - * Key: 策略名称 - * Value: 策略实现 - */ - private final Map strategyRegistry = new ConcurrentHashMap<>(); + @Resource + private OrderLifecycleManager orderLifecycleManager; + + @Resource + private OrderQueueService orderQueueService; + + @Resource + private OpsOrderMapper orderMapper; /** - * 业务类型与策略的映射 - * Key: 业务类型(CLEAN、REPAIR、SECURITY) - * Value: 策略名称 + * 分配策略注册表 + * Key: 业务类型(CLEAN、SECURITY、FACILITIES) + * Value: 分配策略实现 */ - private final Map businessTypeStrategyMap = new ConcurrentHashMap<>(); + private final Map assignStrategyRegistry = new ConcurrentHashMap<>(); + + /** + * 调度策略注册表 + * Key: 业务类型(CLEAN、SECURITY、FACILITIES) + * Value: 调度策略实现 + */ + private final Map scheduleStrategyRegistry = new ConcurrentHashMap<>(); @PostConstruct public void init() { - log.info("派单引擎已初始化,等待策略注册..."); + log.info("调度引擎已初始化,等待策略注册..."); } - // ========== 策略管理 ========== + // ==================== 核心调度方法 ==================== @Override - public void registerStrategy(DispatchStrategy strategy) { - if (strategy == null) { - log.warn("尝试注册空策略,已忽略"); - return; + @BusinessLog( + type = LogType.DISPATCH, + scope = LogScope.ORDER, + description = "工单调度", + includeParams = true, + includeResult = true, + result = "#result.success", + params = {"#context.orderId", "#context.businessType", "#context.priority"} + ) + @Transactional(rollbackFor = Exception.class) + public DispatchResult dispatch(OrderDispatchContext context) { + log.info("开始调度工单: orderId={}, businessType={}, areaId={}, priority={}", + context.getOrderId(), context.getBusinessType(), context.getAreaId(), context.getPriority()); + + // ========== 步骤1:分配决策 - 谁来接单 ========== + AssignStrategy assignStrategy = assignStrategyRegistry.get(context.getBusinessType()); + if (assignStrategy == null) { + log.warn("未找到分配策略: businessType={}", context.getBusinessType()); + return DispatchResult.fail("未找到分配策略: " + context.getBusinessType()); } - String strategyName = strategy.getName(); - String businessType = strategy.getSupportedBusinessType(); - - strategyRegistry.put(strategyName, strategy); - - // 自动注册业务类型映射 - if (businessType != null && !businessType.isEmpty()) { - businessTypeStrategyMap.put(businessType, strategyName); + AssigneeRecommendation recommendation = assignStrategy.recommend(context); + if (recommendation == null || !recommendation.hasRecommendation()) { + log.warn("无可用执行人: orderId={}, businessType={}", context.getOrderId(), context.getBusinessType()); + return DispatchResult.fail("无可用执行人"); } - log.info("派单策略已注册: strategyName={}, businessType={}", - strategyName, businessType); + Long assigneeId = recommendation.getAssigneeId(); + context.setRecommendedAssigneeId(assigneeId); + + log.info("分配决策完成: orderId={}, assigneeId={}, assigneeName={}, reason={}", + context.getOrderId(), assigneeId, recommendation.getAssigneeName(), recommendation.getReason()); + + // ========== 步骤2:调度决策 - 怎么派单 ========== + ScheduleStrategy scheduleStrategy = scheduleStrategyRegistry.get(context.getBusinessType()); + if (scheduleStrategy == null) { + log.warn("未找到调度策略: businessType={}", context.getBusinessType()); + return DispatchResult.fail("未找到调度策略: " + context.getBusinessType()); + } + + // 查询执行人当前状态(由业务层提供 AssigneeStatus 实现) + // 这里通过队列服务获取当前任务信息来判断状态 + List currentTasks = orderQueueService.getTasksByUserId(assigneeId); + context.setCurrentTasks(null); // 由调度策略使用 + + DispatchDecision decision = scheduleStrategy.decide(context); + log.info("调度决策完成: path={}, reason={}", decision.getPath(), decision.getReason()); + + // ========== 步骤3:执行编排 ========== + return executeDispatch(context, decision); } @Override - public void registerBusinessTypeStrategy(String businessType, String strategyName) { + @Transactional(rollbackFor = Exception.class) + public DispatchResult urgentInterrupt(Long urgentOrderId, Long assigneeId) { + log.warn("开始P0紧急插队: urgentOrderId={}, assigneeId={}", urgentOrderId, assigneeId); + + // 查询紧急工单 + OpsOrderDO urgentOrder = orderMapper.selectById(urgentOrderId); + if (urgentOrder == null) { + return DispatchResult.fail("紧急工单不存在: " + urgentOrderId); + } + + // 构建调度上下文 + OrderDispatchContext context = OrderDispatchContext.builder() + .orderId(urgentOrderId) + .orderCode(urgentOrder.getOrderCode()) + .orderTitle(urgentOrder.getTitle()) + .businessType(urgentOrder.getOrderType()) + .areaId(urgentOrder.getAreaId()) + .priority(urgentOrder.getPriorityEnum()) + .recommendedAssigneeId(assigneeId) + .build(); + + // 查询执行人当前任务 + List currentTasks = orderQueueService.getTasksByUserId(assigneeId); + + // 如果有正在执行的任务,需要打断 + OrderQueueDTO currentTask = currentTasks.stream() + .filter(t -> OrderQueueStatusEnum.PROCESSING.getStatus().equals(t.getQueueStatus())) + .findFirst() + .orElse(null); + + if (currentTask != null && !currentTask.getOpsOrderId().equals(urgentOrderId)) { + // 需要打断当前任务 + log.info("打断当前任务: currentOrderId={}, urgentOrderId={}", + currentTask.getOpsOrderId(), urgentOrderId); + + try { + orderLifecycleManager.interruptOrder( + currentTask.getOpsOrderId(), urgentOrderId, assigneeId); + } catch (Exception e) { + log.warn("打断任务失败: currentOrderId={}", currentTask.getOpsOrderId(), e); + } + } + + // 派发紧急任务 + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(urgentOrderId) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .assigneeId(assigneeId) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("P0紧急任务派单") + .build(); + + OrderTransitionResult result = orderLifecycleManager.dispatch(request); + + if (result.isSuccess()) { + return DispatchResult.success("P0紧急任务已派单", assigneeId); + } else { + return DispatchResult.fail("P0紧急任务派单失败: " + result.getMessage()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DispatchResult autoDispatchNext(Long completedOrderId, Long assigneeId) { + log.info("任务完成后自动调度下一个: completedOrderId={}, assigneeId={}", completedOrderId, assigneeId); + + // 1. 优先检查是否有被中断的任务 + List interruptedTasks = orderQueueService.getInterruptedTasksByUserId(assigneeId); + + if (!interruptedTasks.isEmpty()) { + // 恢复第一个中断的任务 + OrderQueueDTO interruptedTask = interruptedTasks.get(0); + log.info("恢复中断任务: orderId={}", interruptedTask.getOpsOrderId()); + + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(interruptedTask.getOpsOrderId()) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .queueId(interruptedTask.getId()) + .assigneeId(assigneeId) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("恢复中断任务") + .build(); + + OrderTransitionResult result = orderLifecycleManager.transition(request); + + if (result.isSuccess()) { + return DispatchResult.success("已恢复中断任务", assigneeId); + } else { + return DispatchResult.fail("恢复中断任务失败: " + result.getMessage()); + } + } + + // 2. 如果没有中断任务,推送队列中的下一个任务 + List waitingTasks = orderQueueService.getWaitingTasksByUserId(assigneeId); + + if (waitingTasks.isEmpty()) { + log.info("无等待任务,执行人变为空闲: assigneeId={}", assigneeId); + // 发布事件,由业务层更新执行人状态 + return DispatchResult.success("无等待任务,任务完成", assigneeId); + } + + // ��送第一个等待任务 + OrderQueueDTO nextTask = waitingTasks.get(0); + log.info("推送下一个等待任务: taskId={}, orderId={}", nextTask.getId(), nextTask.getOpsOrderId()); + + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(nextTask.getOpsOrderId()) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .queueId(nextTask.getId()) + .assigneeId(assigneeId) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("自动推送下一个任务") + .build(); + + OrderTransitionResult result = orderLifecycleManager.transition(request); + + if (result.isSuccess()) { + return DispatchResult.success("已推送下一个任务", assigneeId); + } else { + return DispatchResult.fail("推送下一个任务失败: " + result.getMessage()); + } + } + + // ==================== 策略注册 ==================== + + @Override + public void registerAssignStrategy(String businessType, AssignStrategy strategy) { if (businessType == null || businessType.isEmpty()) { log.warn("业务类型为空,忽略注册"); return; } - - if (!strategyRegistry.containsKey(strategyName)) { - log.warn("策略不存在,无法注册映射: businessType={}, strategyName={}", - businessType, strategyName); + if (strategy == null) { + log.warn("策略为空,忽略注册"); return; } - businessTypeStrategyMap.put(businessType, strategyName); - log.info("业务类型策略映射已注册: businessType={}, strategyName={}", - businessType, strategyName); + assignStrategyRegistry.put(businessType, strategy); + log.info("分配策略已注册: businessType={}, strategyName={}", businessType, strategy.getName()); } @Override - public DispatchStrategy getStrategyByBusinessType(String businessType) { + public void registerScheduleStrategy(String businessType, ScheduleStrategy strategy) { if (businessType == null || businessType.isEmpty()) { - return null; + log.warn("业务类型为空,忽略注册"); + return; } - - 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); + log.warn("策略为空,忽略注册"); + return; } - return strategy; + scheduleStrategyRegistry.put(businessType, strategy); + log.info("调度策略已注册: businessType={}, strategyName={}", businessType, strategy.getName()); } - // ========== 派单决策方法 ========== + // ==================== 决策方法(供外部调用) ==================== @Override - public AssigneeRecommendation recommendAssignee(DispatchContext context) { - if (context == null) { - log.warn("派单上下文为空,无法推荐人员"); - return AssigneeRecommendation.none(); - } - - String businessType = context.getBusinessType(); - DispatchStrategy strategy = getStrategyByBusinessType(businessType); - + public AssigneeRecommendation recommendAssignee(OrderDispatchContext context) { + AssignStrategy strategy = assignStrategyRegistry.get(context.getBusinessType()); if (strategy == null) { - log.warn("未找到业务类型对应的派单策略: businessType={}, orderId={}", - businessType, context.getOrderId()); + log.warn("未找到分配策略: businessType={}", context.getBusinessType()); 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; + return strategy.recommend(context); } catch (Exception e) { - log.error("派单推荐异常: orderId={}, businessType={}", - context.getOrderId(), businessType, e); + log.error("推荐执行人失败: businessType={}", context.getBusinessType(), 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); - + public DispatchDecision decideSchedulePath(OrderDispatchContext context) { + ScheduleStrategy strategy = scheduleStrategyRegistry.get(context.getBusinessType()); if (strategy == null) { - log.warn("未找到业务类型对应的派单策略: businessType={}, orderId={}", - businessType, context.getOrderId()); - return Collections.emptyList(); + log.warn("未找到调度策略: businessType={}", context.getBusinessType()); + return DispatchDecision.unavailable("未找到调度策略"); } 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(); + return strategy.decide(context); } catch (Exception e) { - log.error("批量派单推荐异常: orderId={}, businessType={}", - context.getOrderId(), businessType, e); - return Collections.emptyList(); + log.error("调度决策失败: businessType={}", context.getBusinessType(), e); + return DispatchDecision.unavailable("调度决策异常: " + e.getMessage()); } } @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); - + public InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId, + OrderDispatchContext urgentContext) { + ScheduleStrategy strategy = scheduleStrategyRegistry.get(urgentContext.getBusinessType()); if (strategy == null) { - // 使用默认打断规则 - InterruptDecision decision = defaultInterruptDecision(urgentContext); - log.info("使用默认打断规则: currentAssigneeId={}, urgentOrderId={}, canInterrupt={}", - currentAssigneeId, urgentContext.getOrderId(), decision.canInterrupt()); - return decision; + log.warn("未找到调度策略,使用默认打断规则: businessType={}", urgentContext.getBusinessType()); + return defaultInterruptDecision(urgentContext); } try { - InterruptDecision decision = strategy.evaluateInterrupt( - currentAssigneeId, - null, // currentOrderId 可选 - urgentContext - ); - - log.info("打断评估完成: currentAssigneeId={}, urgentOrderId={}, canInterrupt={}, reason={}", - currentAssigneeId, urgentContext.getOrderId(), - decision.canInterrupt(), decision.getReason()); - - return decision; + return strategy.evaluateInterrupt(currentAssigneeId, currentOrderId, urgentContext); } catch (Exception e) { - log.error("打断评估异常: currentAssigneeId={}, urgentOrderId={}", - currentAssigneeId, urgentContext.getOrderId(), e); + log.error("打断评估失败: businessType={}", urgentContext.getBusinessType(), e); return InterruptDecision.deny("评估异常", "使用默认处理"); } } @@ -223,29 +342,193 @@ public class DispatchEngineImpl implements DispatchEngine { * 默认打断决策 * P0任务可以打断任何非P0任务 */ - private InterruptDecision defaultInterruptDecision(DispatchContext urgentContext) { - if (urgentContext.getPriority() != null && urgentContext.getPriority().isUrgent()) { + private InterruptDecision defaultInterruptDecision(OrderDispatchContext urgentContext) { + if (urgentContext.isUrgent()) { return InterruptDecision.allowByDefault(); } - return InterruptDecision.deny( - "紧急任务优先级不足", - "建议等待当前任务完成" - ); + return InterruptDecision.deny("紧急任务优先级不足", "建议等待当前任务完成"); } - // ========== 查询方法 ========== + // ==================== 私有方法 ==================== /** - * 获取所有已注册的策略 + * 执行调度编排 */ - public List getAllStrategies() { - return new ArrayList<>(strategyRegistry.values()); + private DispatchResult executeDispatch(OrderDispatchContext context, DispatchDecision decision) { + Long orderId = context.getOrderId(); + Long assigneeId = context.getRecommendedAssigneeId(); + + switch (decision.getPath()) { + case DIRECT_DISPATCH: + return executeDirectDispatch(context, assigneeId); + + case PUSH_AND_ENQUEUE: + return executePushAndEnqueue(context, assigneeId); + + case ENQUEUE_ONLY: + return executeEnqueueOnly(context, assigneeId); + + case INTERRUPT_AND_DISPATCH: + return executeInterruptAndDispatch(context, assigneeId, decision.getInterruptedOrderId()); + + case UNAVAILABLE: + return DispatchResult.fail(decision.getReason()); + + default: + return DispatchResult.fail("未知的调度路径: " + decision.getPath()); + } } /** - * 获取所有业务类型与策略的映射 + * 直接派单 */ - public Map getAllBusinessTypeMappings() { - return new java.util.HashMap<>(businessTypeStrategyMap); + private DispatchResult executeDirectDispatch(OrderDispatchContext context, Long assigneeId) { + log.info("执行直接派单: orderId={}, assigneeId={}", context.getOrderId(), assigneeId); + + // 查询是否有队列记录(如果有,说明是自动推送等待任务) + List existingQueues = orderQueueService.getTasksByUserId(assigneeId); + OrderQueueDTO queueDTO = existingQueues.stream() + .filter(q -> q.getOpsOrderId().equals(context.getOrderId())) + .findFirst() + .orElse(null); + + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(context.getOrderId()) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .assigneeId(assigneeId) + .queueId(queueDTO != null ? queueDTO.getId() : null) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("自动派单") + .build(); + + OrderTransitionResult result = orderLifecycleManager.dispatch(request); + + if (result.isSuccess()) { + return DispatchResult.success( + "直接派单成功", + assigneeId, + null, + DispatchPath.DIRECT_DISPATCH, + result.getQueueId() + ); + } else { + return DispatchResult.fail("直接派单失败: " + result.getMessage()); + } + } + + /** + * 推送等待+新任务入队 + */ + private DispatchResult executePushAndEnqueue(OrderDispatchContext context, Long assigneeId) { + log.info("执行推送等待+新任务入队: orderId={}, assigneeId={}", context.getOrderId(), assigneeId); + + // 先推送第一个等待任务(如果有的话) + List waitingTasks = orderQueueService.getWaitingTasksByUserId(assigneeId); + if (!waitingTasks.isEmpty()) { + OrderQueueDTO firstWaiting = waitingTasks.get(0); + OrderTransitionRequest dispatchRequest = OrderTransitionRequest.builder() + .orderId(firstWaiting.getOpsOrderId()) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .assigneeId(assigneeId) + .queueId(firstWaiting.getId()) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("自动推送等待任务") + .build(); + + orderLifecycleManager.dispatch(dispatchRequest); + log.info("已推送等待任务: taskId={}", firstWaiting.getId()); + } + + // 新任务入队 + OrderTransitionRequest enqueueRequest = OrderTransitionRequest.builder() + .orderId(context.getOrderId()) + .assigneeId(assigneeId) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("执行人忙碌,任务入队") + .build(); + + OrderTransitionResult result = orderLifecycleManager.enqueue(enqueueRequest); + + if (result.isSuccess()) { + return DispatchResult.success( + "已推送等待任务,新任务已入队", + assigneeId, + null, + DispatchPath.PUSH_AND_ENQUEUE, + result.getQueueId() + ); + } else { + return DispatchResult.fail("入队失败: " + result.getMessage()); + } + } + + /** + * 仅入队 + */ + private DispatchResult executeEnqueueOnly(OrderDispatchContext context, Long assigneeId) { + log.info("执行仅入队: orderId={}, assigneeId={}", context.getOrderId(), assigneeId); + + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(context.getOrderId()) + .assigneeId(assigneeId) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("执行人忙碌,任务入队") + .build(); + + OrderTransitionResult result = orderLifecycleManager.enqueue(request); + + if (result.isSuccess()) { + return DispatchResult.success( + "任务已入队", + assigneeId, + null, + DispatchPath.ENQUEUE_ONLY, + result.getQueueId() + ); + } else { + return DispatchResult.fail("入队失败: " + result.getMessage()); + } + } + + /** + * 打断并派单 + */ + private DispatchResult executeInterruptAndDispatch(OrderDispatchContext context, Long assigneeId, + Long interruptedOrderId) { + log.warn("执行打断并派单: orderId={}, assigneeId={}, interruptedOrderId={}", + context.getOrderId(), assigneeId, interruptedOrderId); + + // 先打断当前任务 + if (interruptedOrderId != null) { + orderLifecycleManager.interruptOrder(interruptedOrderId, context.getOrderId(), assigneeId); + } + + // 派发紧急任务 + OrderTransitionRequest request = OrderTransitionRequest.builder() + .orderId(context.getOrderId()) + .targetStatus(WorkOrderStatusEnum.DISPATCHED) + .assigneeId(assigneeId) + .operatorType(OperatorTypeEnum.SYSTEM) + .operatorId(assigneeId) + .reason("P0紧急任务派单") + .build(); + + OrderTransitionResult result = orderLifecycleManager.dispatch(request); + + if (result.isSuccess()) { + return DispatchResult.success( + "P0紧急任务已派单", + assigneeId, + null, + DispatchPath.INTERRUPT_AND_DISPATCH, + result.getQueueId() + ); + } else { + return DispatchResult.fail("P0紧急任务派单失败: " + result.getMessage()); + } } } 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 deleted file mode 100644 index f6a878fa..00000000 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchStrategy.java +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index 9291d6d1..00000000 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/InterruptDecision.java +++ /dev/null @@ -1,69 +0,0 @@ -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(); - } -} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/AssigneeRecommendation.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/AssigneeRecommendation.java new file mode 100644 index 00000000..bb820ed4 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/AssigneeRecommendation.java @@ -0,0 +1,79 @@ +package com.viewsh.module.ops.core.dispatch.model; + +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; + + /** + * 所属区域ID + */ + private Long areaId; + + /** + * 是否有推荐结果 + */ + public boolean hasRecommendation() { + return assigneeId != null; + } + + /** + * 空推荐结果 + */ + public static AssigneeRecommendation none() { + return new AssigneeRecommendation(); + } + + /** + * 创建推荐结果 + * + * @param assigneeId 执行人ID + * @param assigneeName 执行人姓名 + * @param score 评分 + * @param reason 推荐理由 + */ + public static AssigneeRecommendation of(Long assigneeId, String assigneeName, + Integer score, String reason) { + return AssigneeRecommendation.builder() + .assigneeId(assigneeId) + .assigneeName(assigneeName) + .score(score) + .reason(reason) + .build(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/AssigneeStatus.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/AssigneeStatus.java new file mode 100644 index 00000000..51e097fe --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/AssigneeStatus.java @@ -0,0 +1,107 @@ +package com.viewsh.module.ops.core.dispatch.model; + +/** + * 执行人状态通用接口 + *

+ * 调度引擎通过此接口获取执行人状态,不依赖具体业务实现。 + * 不同业务线(保洁、安保、工程等)需要实现此接口。 + * + * @author lzh + */ +public interface AssigneeStatus { + + /** + * 获取状态码 + *

+ * 常见值:IDLE(空闲)、BUSY(忙碌)、OFFLINE(离线) + * + * @return 状态码 + */ + String getStatus(); + + /** + * 是否空闲 + * + * @return true=空闲,false=忙碌或离线 + */ + boolean isIdle(); + + /** + * 是否忙碌 + * + * @return true=忙碌,false=空闲或离线 + */ + boolean isBusy(); + + /** + * 是否在线 + * + * @return true=在线,false=离线 + */ + boolean isOnline(); + + /** + * 获取当前正在执行的任务数 + * + * @return 当前任务数 + */ + Long getCurrentTaskCount(); + + /** + * 获取等待中的任务数 + * + * @return 等待任务数 + */ + Long getWaitingTaskCount(); + + /** + * 获取执行人ID + * + * @return 执行人ID + */ + Long getAssigneeId(); + + /** + * 获取执行人姓名 + * + * @return 执行人姓名 + */ + String getAssigneeName(); + + /** + * 获取所属区域ID + * + * @return 区域ID + */ + Long getAreaId(); + + /** + * 获取最后心跳时间 + * + * @return 心跳时间 + */ + java.time.LocalDateTime getLastHeartbeatTime(); + + /** + * 获取电量(百分比) + *

+ * 仅适用于使用设备的执行人 + * + * @return 电量百分比,0-100 + */ + default Integer getBatteryLevel() { + return null; + } + + /** + * 获取扩展属性 + *

+ * 用于支持业务特定的属性 + * + * @param key 属性键 + * @return 属性值 + */ + default Object getExtension(String key) { + return null; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchDecision.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchDecision.java new file mode 100644 index 00000000..3e3513a3 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchDecision.java @@ -0,0 +1,95 @@ +package com.viewsh.module.ops.core.dispatch.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 调度决策 + *

+ * 调度策略根据执行人状态决策的调度路径 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DispatchDecision { + + /** + * 调度路径 + */ + private DispatchPath path; + + /** + * 决策理由 + */ + private String reason; + + /** + * 需要打断的工单ID(仅在 INTERRUPT_AND_DISPATCH 路径时有值) + */ + private Long interruptedOrderId; + + /** + * 直接派单决策 + */ + public static DispatchDecision directDispatch() { + return DispatchDecision.builder() + .path(DispatchPath.DIRECT_DISPATCH) + .reason("执行人空闲,直接派单") + .build(); + } + + /** + * 推送等待+新任务入队决策 + */ + public static DispatchDecision pushAndEnqueue() { + return DispatchDecision.builder() + .path(DispatchPath.PUSH_AND_ENQUEUE) + .reason("执行人空闲但有等待任务,推送等待任务+新任务入队") + .build(); + } + + /** + * 仅入队决策 + */ + public static DispatchDecision enqueueOnly() { + return DispatchDecision.builder() + .path(DispatchPath.ENQUEUE_ONLY) + .reason("执行人忙碌,任务入队等待") + .build(); + } + + /** + * 打断并派单决策 + * + * @param interruptedOrderId 需要打断的工单ID + */ + public static DispatchDecision interruptAndDispatch(Long interruptedOrderId) { + return DispatchDecision.builder() + .path(DispatchPath.INTERRUPT_AND_DISPATCH) + .reason("P0紧急任务,打断当前任务") + .interruptedOrderId(interruptedOrderId) + .build(); + } + + /** + * 无法派单决策 + */ + public static DispatchDecision unavailable(String reason) { + return DispatchDecision.builder() + .path(DispatchPath.UNAVAILABLE) + .reason(reason) + .build(); + } + + /** + * 是否可以派单 + */ + public boolean canDispatch() { + return path != null && path != DispatchPath.UNAVAILABLE; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchPath.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchPath.java new file mode 100644 index 00000000..93816b42 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchPath.java @@ -0,0 +1,84 @@ +package com.viewsh.module.ops.core.dispatch.model; + +/** + * 调度路径枚举 + *

+ * 定义工单调度的可能路径 + * + * @author lzh + */ +public enum DispatchPath { + + /** + * 直接派单 + *

+ * 场景:执行人空闲且无正在执行的任务 + *

+ * 流程:工单 QUEUED → DISPATCHED,队列 WAITING → PROCESSING + */ + DIRECT_DISPATCH("direct_dispatch", "直接派单"), + + /** + * 推送等待任务+新任务入队 + *

+ * 场景:执行人空闲但有等待中的任务 + *

+ * 流程:先推送第一个等待任务,新任务入队 + */ + PUSH_AND_ENQUEUE("push_and_enqueue", "推送等待+新任务入队"), + + /** + * 仅入队 + *

+ * 场景:执行人忙碌且非P0优先级 + *

+ * 流程:工单 PENDING → QUEUED,队列创建 WAITING + */ + ENQUEUE_ONLY("enqueue_only", "仅入队"), + + /** + * 打断并派单 + *

+ * 场景:P0紧急任务,执行人正在执行其他任务且可以打断 + *

+ * 流程:原任务 → PAUSED,新任务 → DISPATCHED + */ + INTERRUPT_AND_DISPATCH("interrupt_and_dispatch", "打断并派单"), + + /** + * 无法派单 + *

+ * 场景:无可用执行人或其他无法派单的情况 + */ + UNAVAILABLE("unavailable", "无法派单"); + + private final String code; + private final String description; + + DispatchPath(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 是否需要入队 + */ + public boolean needEnqueue() { + return this == ENQUEUE_ONLY || this == INTERRUPT_AND_DISPATCH || this == PUSH_AND_ENQUEUE; + } + + /** + * 是否需要立即派单 + */ + public boolean needDispatch() { + return this == DIRECT_DISPATCH || this == INTERRUPT_AND_DISPATCH; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchResult.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchResult.java new file mode 100644 index 00000000..c9d65734 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/DispatchResult.java @@ -0,0 +1,86 @@ +package com.viewsh.module.ops.core.dispatch.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 调度结果 + *

+ * 调度引擎执行后的返回结果 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DispatchResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 结果消息 + */ + private String message; + + /** + * 分配的执行人ID + */ + private Long assigneeId; + + /** + * 分配的执行人姓名 + */ + private String assigneeName; + + /** + * 调度路径 + */ + private DispatchPath path; + + /** + * 队列ID(如果已入队) + */ + private Long queueId; + + /** + * 成功结果 + */ + public static DispatchResult success(String message, Long assigneeId) { + return DispatchResult.builder() + .success(true) + .message(message) + .assigneeId(assigneeId) + .build(); + } + + /** + * 成功结果(完整信息) + */ + public static DispatchResult success(String message, Long assigneeId, String assigneeName, + DispatchPath path, Long queueId) { + return DispatchResult.builder() + .success(true) + .message(message) + .assigneeId(assigneeId) + .assigneeName(assigneeName) + .path(path) + .queueId(queueId) + .build(); + } + + /** + * 失败结果 + */ + public static DispatchResult fail(String message) { + return DispatchResult.builder() + .success(false) + .message(message) + .build(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/OrderDispatchContext.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/OrderDispatchContext.java new file mode 100644 index 00000000..d809d3b3 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/model/OrderDispatchContext.java @@ -0,0 +1,120 @@ +package com.viewsh.module.ops.core.dispatch.model; + +import com.viewsh.module.ops.enums.PriorityEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 调度上下文 + *

+ * 调度引擎执行调度时需要的上下文信息 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderDispatchContext { + + /** + * 工单ID + */ + private Long orderId; + + /** + * 工单编号 + */ + private String orderCode; + + /** + * 工单标题 + */ + private String orderTitle; + + /** + * 业务类型 + *

+ * 例如:CLEAN(保洁)、SECURITY(安保)、FACILITIES(工程) + */ + private String businessType; + + /** + * 区域ID + */ + private Long areaId; + + /** + * 工单优先级 + */ + private PriorityEnum priority; + + /** + * 推荐的执行人ID + *

+ * 由分配策略推荐后填充 + */ + private Long recommendedAssigneeId; + + /** + * 执行人当前状态 + *

+ * 由调度引擎查询后填充 + */ + private AssigneeStatus assigneeStatus; + + /** + * 当前任务列表 + *

+ * 执行人当前正在执行和等待的任务 + */ + private Map currentTasks; + + /** + * 紧急工单ID + *

+ * 当此工单是P0紧急任务,需要打断其他任务时,记录被打断的工单ID + */ + private Long interruptedOrderId; + + /** + * 扩展信息 + *

+ * 用于支持业务特定的扩展信息 + */ + private Map payload; + + /** + * 是否为P0紧急任务 + */ + public boolean isUrgent() { + return priority != null && priority.isUrgent(); + } + + /** + * 获取扩展信息 + * + * @param key 键 + * @return 值 + */ + public Object getPayload(String key) { + return payload != null ? payload.get(key) : null; + } + + /** + * 设置扩展信息 + * + * @param key 键 + * @param value 值 + */ + public void putPayload(String key, Object value) { + if (payload == null) { + payload = new java.util.HashMap<>(); + } + payload.put(key, value); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/AssignStrategy.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/AssignStrategy.java new file mode 100644 index 00000000..584c6cd0 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/AssignStrategy.java @@ -0,0 +1,58 @@ +package com.viewsh.module.ops.core.dispatch.strategy; + +import com.viewsh.module.ops.core.dispatch.model.AssigneeRecommendation; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; + +/** + * 分配策略接口 + *

+ * 负责决策:谁来接单 + *

+ * 不同业务线实现此接口,提供各自的分配逻辑。 + * 例如:保洁业务按区域优先分配,安保业务按技能匹配。 + * + * @author lzh + */ +public interface AssignStrategy { + + /** + * 获取策略名称 + * + * @return 策略名称 + */ + String getName(); + + /** + * 获取支持的业务类型 + * + * @return 业务类型,例如:CLEAN、SECURITY、FACILITIES + */ + String getSupportedBusinessType(); + + /** + * 推荐执行人 + *

+ * 根据工单信息推荐最合适的执行人 + * + * @param context 调度上下文 + * @return 推荐结果,无合适执行人时返回 null + */ + AssigneeRecommendation recommend(OrderDispatchContext context); + + /** + * 批量推荐执行人 + *

+ * 用于备用人员场景 + * + * @param context 调度上下文 + * @param limit 最多返回数量 + * @return 推荐结果列表,按评分降序排列 + */ + default java.util.List recommendBatch(OrderDispatchContext context, int limit) { + AssigneeRecommendation recommendation = recommend(context); + if (recommendation == null || !recommendation.hasRecommendation()) { + return java.util.Collections.emptyList(); + } + return java.util.Collections.singletonList(recommendation); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/InterruptDecision.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/InterruptDecision.java new file mode 100644 index 00000000..6cfb57af --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/InterruptDecision.java @@ -0,0 +1,68 @@ +package com.viewsh.module.ops.core.dispatch.strategy; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 打断决策 + *

+ * P0紧急任务时,是否可以打断当前正在执行的任务 + * + * @author lzh + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InterruptDecision { + + /** + * 是否允许打断 + */ + private boolean allowed; + + /** + * 拒绝原因(当不允许时) + */ + private String denyReason; + + /** + * 建议(当不允许时) + */ + private String suggestion; + + /** + * 允许打断(使用默认理由) + */ + public static InterruptDecision allowByDefault() { + return InterruptDecision.builder() + .allowed(true) + .denyReason(null) + .suggestion(null) + .build(); + } + + /** + * 允许打断(自定义理由) + */ + public static InterruptDecision allow(String reason) { + return InterruptDecision.builder() + .allowed(true) + .denyReason(reason) + .suggestion(null) + .build(); + } + + /** + * 拒绝打断 + */ + public static InterruptDecision deny(String reason, String suggestion) { + return InterruptDecision.builder() + .allowed(false) + .denyReason(reason) + .suggestion(suggestion) + .build(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/ScheduleStrategy.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/ScheduleStrategy.java new file mode 100644 index 00000000..062e9f04 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/strategy/ScheduleStrategy.java @@ -0,0 +1,65 @@ +package com.viewsh.module.ops.core.dispatch.strategy; + +import com.viewsh.module.ops.core.dispatch.model.DispatchDecision; +import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; + +/** + * 调度策略接口 + *

+ * 负责决策:怎么派单 + *

+ * 根据执行人当前状态决定调度路径: + *

    + *
  • 空闲无任务 → 直接派单 (DIRECT_DISPATCH)
  • + *
  • 空闲有等待 → 推送等待任务 + 新任务入队 (PUSH_AND_ENQUEUE)
  • + *
  • 忙碌且非P0 → 入队等待 (ENQUEUE_ONLY)
  • + *
  • 忙碌且P0 → 评估打断 (INTERRUPT_AND_DISPATCH)
  • + *
+ * + * @author lzh + */ +public interface ScheduleStrategy { + + /** + * 获取策略名称 + * + * @return 策略名称 + */ + String getName(); + + /** + * 获取支持的业务类型 + * + * @return 业务类型,例如:CLEAN、SECURITY、FACILITIES + */ + String getSupportedBusinessType(); + + /** + * 决策调度路径 + *

+ * 根据调度上下文(包含推荐执行人信息)决定调度路径 + * + * @param context 调度上下文 + * @return 调度决策 + */ + DispatchDecision decide(OrderDispatchContext context); + + /** + * 评估是否可以打断 + *

+ * P0紧急任务时,评估是否可以打断当前正在执行的任务 + * + * @param currentAssigneeId 当前执行人ID + * @param currentOrderId 当前工单ID + * @param urgentContext 紧急任务上下文 + * @return 打断决策 + */ + default InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId, + OrderDispatchContext urgentContext) { + // 默认实现:P0任务可以打断,其他不能 + if (urgentContext.isUrgent()) { + return InterruptDecision.allowByDefault(); + } + return InterruptDecision.deny("紧急任务优先级不足", "建议等待当前任务完成"); + } +}