chore: 【ops】调度引擎重新调整

This commit is contained in:
lzh
2026-01-09 16:45:44 +08:00
parent 285abe6d48
commit 8feb778291
15 changed files with 1286 additions and 533 deletions

View File

@@ -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;
}
}

View File

@@ -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<String, Integer> skillRequirements;
/**
* 排除的执行人员ID列表可选
* 例如:已分配过该工单的人员
*/
private Set<Long> excludedAssigneeIds;
/**
* 首选执行人员ID可选
* 例如:指定的保洁员
*/
private Long preferredAssigneeId;
/**
* 扩展参数(可选)
* 用于传递业务特定的参数
*/
private Map<String, Object> 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> T getExtraParam(String key, Class<T> 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);
}
}

View File

@@ -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;
/**
* 派单引擎 - 纯决策层
* 调度引擎 - 统一派单入口
* <p>
* 职责:
* 1. 根据策略推荐最合适的执行人员
* 2. 不涉及状态管理(队列状态、工单状态由各自服务管理
* 3. 不涉及设备通知(由通知服务处理)
* 1. 分配决策:谁来接单(通过 AssignStrategy
* 2. 调度决策:怎么派单(通过 ScheduleStrategy
* 3. 执行编排:协调生命周期管理器完成派单
* <p>
* 设计原则:
* - 单一职责:只负责派单决策
* - 开闭原则:通过策略模式支持扩展
* - 依赖倒置:业务层依赖接口而非实现
* - 纯通用逻辑:不包含任何业务特定逻辑(如保洁员状态)
* - 事件驱动:业务特定状态通过事件监听实现
* - 可扩展:通过策略模式支持不同业务线
*
* @author lzh
*/
public interface DispatchEngine {
/**
* 推荐执行人员(核心方法)
* <p>
* 根据派单上下文(区域、优先级、技能要求等)推荐最合适的执行人员
*
* @param context 派单上下文
* @return 推荐结果,如果没有合适的返回 AssigneeRecommendation.none()
*/
AssigneeRecommendation recommendAssignee(DispatchContext context);
// ==================== 核心调度方法 ====================
/**
* 批量推荐执行人员
* 调度工单(核心方法)
* <p>
* 用于场景:需要从多个候选人中选择最优人员,或需要备用人员
* 完整的调度流程:
* <ol>
* <li>分配决策:通过 AssignStrategy 推荐执行人</li>
* <li>调度决策:通过 ScheduleStrategy 决定调度路径</li>
* <li>执行编排:协调生命周期管理器完成状态转换</li>
* </ol>
*
* @param context 派单上下文
* @param limit 返回结果数量限制
* @return 推荐结果列表,按匹配分数降序排序
* @param context 调度上下文
* @return 调度结果
*/
List<AssigneeRecommendation> recommendAssignees(DispatchContext context, int limit);
DispatchResult dispatch(OrderDispatchContext context);
/**
* 评估是否可以打断当前任务
* P0紧急任务插队
* <p>
* 用于P0紧急任务插队场景:判断是否可以打断当前执行人员正在执行的任务
* P0紧急任务需要派单时:
* <ol>
* <li>分配决策:推荐执行人</li>
* <li>评估打断:判断是否可以打断当前任务</li>
* <li>执行编排:打断当前任务 + 派发紧急任务</li>
* </ol>
*
* @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);
/**
* 注册派单策略
* 任务完成后自动调度下一个
* <p>
* 各业务线(保洁、安保、维修等)实现自己的派单策略,注册到引擎中
* 优先级:
* <ol>
* <li>检查是否有被中断的任务,优先恢复</li>
* <li>如果没有中断任务,推送队列中的下一个</li>
* <li>如果没有等待任务,通知业务层更新执行人状态</li>
* </ol>
*
* @param strategy 派单策略实现
* @param completedOrderId 已完成的工单ID
* @param assigneeId 执行人ID
* @return 调度结果
*/
void registerStrategy(DispatchStrategy strategy);
DispatchResult autoDispatchNext(Long completedOrderId, Long assigneeId);
// ==================== 策略注册 ====================
/**
* 注册业务类型与策略的映射
* <p>
* 建立业务类型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);
// ==================== 决策方法(供外部调用) ====================
/**
* 推荐执行人
* <p>
* 纯决策方法,不涉及状态变更
*
* @param context 调度上下文
* @return 推荐结果
*/
AssigneeRecommendation recommendAssignee(OrderDispatchContext context);
/**
* 决策调度路径
* <p>
* 纯决策方法,不涉及状态变更
*
* @param context 调度上下文
* @return 调度决策
*/
DispatchDecision decideSchedulePath(OrderDispatchContext context);
/**
* 评估是否可以打断
* <p>
* 纯决策方法,不涉及状态变更
*
* @param currentAssigneeId 当前执行人ID
* @param currentOrderId 当前工单ID
* @param urgentContext 紧急任务上下文
* @return 打断决策
*/
InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId,
OrderDispatchContext urgentContext);
}

View File

@@ -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;
/**
* 派单引擎实现
* 调度引擎实现
* <p>
* 职责:
* 1. 管理派单策略的注册和查找
* 2. 根据业务类型路由到对应的策略
* 3. 提供统一的派单决策接口
* 1. 管理策略的注册和查找
* 2. 执行调度流程:分配决策 → 调度决策 → 执行编排
* 3. 协调生命周期管理器完成状态转换
* <p>
* 注意
* - 这是纯决策层,不涉及任何状态管理
* - 队列状态、工单状态由各自服务管理
* - 设备通知由通知服务处理
* 设计原则
* - 纯通用逻辑:不包含任何业务特定逻辑
* - 事件驱动:业务特定状态通过事件监听实现
*
* @author lzh
*/
@@ -30,191 +45,295 @@ import java.util.concurrent.ConcurrentHashMap;
@Service
public class DispatchEngineImpl implements DispatchEngine {
/**
* 派单策略注册表
* Key: 策略名称
* Value: 策略实现
*/
private final Map<String, DispatchStrategy> 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<String, String> businessTypeStrategyMap = new ConcurrentHashMap<>();
private final Map<String, AssignStrategy> assignStrategyRegistry = new ConcurrentHashMap<>();
/**
* 调度策略注册表
* Key: 业务类型CLEAN、SECURITY、FACILITIES
* Value: 调度策略实现
*/
private final Map<String, ScheduleStrategy> 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<OrderQueueDTO> 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<OrderQueueDTO> 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<OrderQueueDTO> 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<OrderQueueDTO> waitingTasks = orderQueueService.getWaitingTasksByUserId(assigneeId);
if (waitingTasks.isEmpty()) {
log.info("无等待任务,执行人变为空闲: assigneeId={}", assigneeId);
// 发布事件,由业务层更新执行人状态
return DispatchResult.success("无等待任务,任务完成", assigneeId);
}
// <20><>送第一个等待任务
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<AssigneeRecommendation> 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<AssigneeRecommendation> 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<DispatchStrategy> 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<String, String> getAllBusinessTypeMappings() {
return new java.util.HashMap<>(businessTypeStrategyMap);
private DispatchResult executeDirectDispatch(OrderDispatchContext context, Long assigneeId) {
log.info("执行直接派单: orderId={}, assigneeId={}", context.getOrderId(), assigneeId);
// 查询是否有队列记录(如果有,说明是自动推送等待任务)
List<OrderQueueDTO> 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<OrderQueueDTO> 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());
}
}
}

View File

@@ -1,85 +0,0 @@
package com.viewsh.module.ops.core.dispatch;
import java.util.List;
/**
* 派单策略接口
* <p>
* 各业务模块(保洁、安保、工程等)需要实现此接口,定义自己的派单逻辑
* <p>
* 职责:
* 1. 根据派单上下文推荐合适的执行人员
* 2. 判断是否可以打断当前任务
*
* @author lzh
*/
public interface DispatchStrategy {
/**
* 策略名称
* <p>
* 如cleaner_area_priority, security_skill_match
* 命名规范:{业务类型}_{策略描述}
*
* @return 策略名称
*/
String getName();
/**
* 支持的业务类型
* <p>
* 如CLEAN、REPAIR、SECURITY
*
* @return 业务类型
*/
String getSupportedBusinessType();
/**
* 执行派单策略,推荐执行人员
* <p>
* 根据派单上下文(区域、优先级、技能要求等)推荐最合适的执行人员
*
* @param context 派单上下文
* @return 推荐结果,如果没有合适的返回 AssigneeRecommendation.none()
*/
AssigneeRecommendation recommendAssignee(DispatchContext context);
/**
* 批量推荐执行人员
* <p>
* 用于场景:需要从多个候选人中选择,或需要备用人员
*
* @param context 派单上下文
* @param limit 返回结果数量限制
* @return 推荐结果列表,按匹配分数降序排序
*/
List<AssigneeRecommendation> recommendAssignees(DispatchContext context, int limit);
/**
* 评估是否可以打断当前任务
* <p>
* 当P0紧急任务需要插队时判断是否可以打断当前执行的任务
*
* @param currentAssigneeId 当前执行任务的执行人员ID
* @param currentOrderId 当前正在执行的工单ID可选
* @param urgentContext 紧急任务的派单上下文
* @return 打断决策结果
*/
InterruptDecision evaluateInterrupt(Long currentAssigneeId, Long currentOrderId, DispatchContext urgentContext);
/**
* 默认实现:判断是否可以打断
* <p>
* 默认规则P0任务可以打断任何非P0任务
*/
default InterruptDecision defaultEvaluateInterrupt(Long currentAssigneeId, Long currentOrderId,
DispatchContext urgentContext) {
if (urgentContext.getPriority() != null && urgentContext.getPriority().isUrgent()) {
return InterruptDecision.allowByDefault();
}
return InterruptDecision.deny(
"紧急任务优先级不足",
"建议等待当前任务完成"
);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,79 @@
package com.viewsh.module.ops.core.dispatch.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 执行人推荐结果
* <p>
* 分配策略推荐的执行人信息
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AssigneeRecommendation {
/**
* 推荐的执行人ID
*/
private Long assigneeId;
/**
* 推荐的执行人姓名
*/
private String assigneeName;
/**
* 推荐评分0-100
* <p>
* 评分越高表示越适合
*/
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();
}
}

View File

@@ -0,0 +1,107 @@
package com.viewsh.module.ops.core.dispatch.model;
/**
* 执行人状态通用接口
* <p>
* 调度引擎通过此接口获取执行人状态,不依赖具体业务实现。
* 不同业务线(保洁、安保、工程等)需要实现此接口。
*
* @author lzh
*/
public interface AssigneeStatus {
/**
* 获取状态码
* <p>
* 常见值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();
/**
* 获取电量(百分比)
* <p>
* 仅适用于使用设备的执行人
*
* @return 电量百分比0-100
*/
default Integer getBatteryLevel() {
return null;
}
/**
* 获取扩展属性
* <p>
* 用于支持业务特定的属性
*
* @param key 属性键
* @return 属性值
*/
default Object getExtension(String key) {
return null;
}
}

View File

@@ -0,0 +1,95 @@
package com.viewsh.module.ops.core.dispatch.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 调度决策
* <p>
* 调度策略根据执行人状态决策的调度路径
*
* @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;
}
}

View File

@@ -0,0 +1,84 @@
package com.viewsh.module.ops.core.dispatch.model;
/**
* 调度路径枚举
* <p>
* 定义工单调度的可能路径
*
* @author lzh
*/
public enum DispatchPath {
/**
* 直接派单
* <p>
* 场景:执行人空闲且无正在执行的任务
* <p>
* 流程:工单 QUEUED → DISPATCHED队列 WAITING → PROCESSING
*/
DIRECT_DISPATCH("direct_dispatch", "直接派单"),
/**
* 推送等待任务+新任务入队
* <p>
* 场景:执行人空闲但有等待中的任务
* <p>
* 流程:先推送第一个等待任务,新任务入队
*/
PUSH_AND_ENQUEUE("push_and_enqueue", "推送等待+新任务入队"),
/**
* 仅入队
* <p>
* 场景执行人忙碌且非P0优先级
* <p>
* 流程:工单 PENDING → QUEUED队列创建 WAITING
*/
ENQUEUE_ONLY("enqueue_only", "仅入队"),
/**
* 打断并派单
* <p>
* 场景P0紧急任务执行人正在执行其他任务且可以打断
* <p>
* 流程:原任务 → PAUSED新任务 → DISPATCHED
*/
INTERRUPT_AND_DISPATCH("interrupt_and_dispatch", "打断并派单"),
/**
* 无法派单
* <p>
* 场景:无可用执行人或其他无法派单的情况
*/
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;
}
}

View File

@@ -0,0 +1,86 @@
package com.viewsh.module.ops.core.dispatch.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 调度结果
* <p>
* 调度引擎执行后的返回结果
*
* @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();
}
}

View File

@@ -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;
/**
* 调度上下文
* <p>
* 调度引擎执行调度时需要的上下文信息
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderDispatchContext {
/**
* 工单ID
*/
private Long orderId;
/**
* 工单编号
*/
private String orderCode;
/**
* 工单标题
*/
private String orderTitle;
/**
* 业务类型
* <p>
* 例如CLEAN保洁、SECURITY安保、FACILITIES工程
*/
private String businessType;
/**
* 区域ID
*/
private Long areaId;
/**
* 工单优先级
*/
private PriorityEnum priority;
/**
* 推荐的执行人ID
* <p>
* 由分配策略推荐后填充
*/
private Long recommendedAssigneeId;
/**
* 执行人当前状态
* <p>
* 由调度引擎查询后填充
*/
private AssigneeStatus assigneeStatus;
/**
* 当前任务列表
* <p>
* 执行人当前正在执行和等待的任务
*/
private Map<String, Object> currentTasks;
/**
* 紧急工单ID
* <p>
* 当此工单是P0紧急任务需要打断其他任务时记录被打断的工单ID
*/
private Long interruptedOrderId;
/**
* 扩展信息
* <p>
* 用于支持业务特定的扩展信息
*/
private Map<String, Object> 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);
}
}

View File

@@ -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;
/**
* 分配策略接口
* <p>
* 负责决策:谁来接单
* <p>
* 不同业务线实现此接口,提供各自的分配逻辑。
* 例如:保洁业务按区域优先分配,安保业务按技能匹配。
*
* @author lzh
*/
public interface AssignStrategy {
/**
* 获取策略名称
*
* @return 策略名称
*/
String getName();
/**
* 获取支持的业务类型
*
* @return 业务类型例如CLEAN、SECURITY、FACILITIES
*/
String getSupportedBusinessType();
/**
* 推荐执行人
* <p>
* 根据工单信息推荐最合适的执行人
*
* @param context 调度上下文
* @return 推荐结果,无合适执行人时返回 null
*/
AssigneeRecommendation recommend(OrderDispatchContext context);
/**
* 批量推荐执行人
* <p>
* 用于备用人员场景
*
* @param context 调度上下文
* @param limit 最多返回数量
* @return 推荐结果列表,按评分降序排列
*/
default java.util.List<AssigneeRecommendation> 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);
}
}

View File

@@ -0,0 +1,68 @@
package com.viewsh.module.ops.core.dispatch.strategy;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 打断决策
* <p>
* 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();
}
}

View File

@@ -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;
/**
* 调度策略接口
* <p>
* 负责决策:怎么派单
* <p>
* 根据执行人当前状态决定调度路径:
* <ul>
* <li>空闲无任务 → 直接派单 (DIRECT_DISPATCH)</li>
* <li>空闲有等待 → 推送等待任务 + 新任务入队 (PUSH_AND_ENQUEUE)</li>
* <li>忙碌且非P0 → 入队等待 (ENQUEUE_ONLY)</li>
* <li>忙碌且P0 → 评估打断 (INTERRUPT_AND_DISPATCH)</li>
* </ul>
*
* @author lzh
*/
public interface ScheduleStrategy {
/**
* 获取策略名称
*
* @return 策略名称
*/
String getName();
/**
* 获取支持的业务类型
*
* @return 业务类型例如CLEAN、SECURITY、FACILITIES
*/
String getSupportedBusinessType();
/**
* 决策调度路径
* <p>
* 根据调度上下文(包含推荐执行人信息)决定调度路径
*
* @param context 调度上下文
* @return 调度决策
*/
DispatchDecision decide(OrderDispatchContext context);
/**
* 评估是否可以打断
* <p>
* 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("紧急任务优先级不足", "建议等待当前任务完成");
}
}