chore: 【ops】派单策略逻辑编写(暂时预留、后期设计接入)

This commit is contained in:
lzh
2026-01-08 15:14:08 +08:00
parent 97f30356d8
commit d9b335c6c9
6 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
package com.viewsh.module.ops.core.dispatch;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 派单推荐结果
* 包含推荐的执行人员及推荐理由
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AssigneeRecommendation {
/**
* 推荐的执行人员ID
*/
private Long assigneeId;
/**
* 执行人员姓名
*/
private String assigneeName;
/**
* 匹配分数0-100
* 分数越高表示越匹配
*/
private Integer score;
/**
* 推荐理由
* 例如:"同区域、电量充足、当前空闲"
*/
private String reason;
/**
* 创建空推荐结果(表示没有合适的人员)
*/
public static AssigneeRecommendation none() {
return new AssigneeRecommendation(null, null, 0, "无可用人员");
}
/**
* 创建推荐结果
*/
public static AssigneeRecommendation of(Long assigneeId, String assigneeName, Integer score, String reason) {
return new AssigneeRecommendation(assigneeId, assigneeName, score, reason);
}
/**
* 是否有推荐结果
*/
public boolean hasRecommendation() {
return assigneeId != null;
}
}

View File

@@ -0,0 +1,127 @@
package com.viewsh.module.ops.core.dispatch;
import com.viewsh.module.ops.enums.PriorityEnum;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
import java.util.Set;
/**
* 派单上下文
* 封装派单决策所需的所有信息
*
* @author lzh
*/
@Data
@Builder
public class DispatchContext {
/**
* 业务类型CLEAN、REPAIR、SECURITY等
*/
private String businessType;
/**
* 工单ID
*/
private Long orderId;
/**
* 区域ID
*/
private Long areaId;
/**
* 优先级
*/
private PriorityEnum priority;
/**
* 技能要求(可选)
* Key: 技能类型, Value: 技能等级
*/
private Map<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

@@ -0,0 +1,80 @@
package com.viewsh.module.ops.core.dispatch;
import java.util.List;
/**
* 派单引擎 - 纯决策层
* <p>
* 职责:
* 1. 根据策略推荐最合适的执行人员
* 2. 不涉及状态管理(队列状态、工单状态由各自服务管理)
* 3. 不涉及设备通知(由通知服务处理)
* <p>
* 设计原则:
* - 单一职责:只负责派单决策
* - 开闭原则:通过策略模式支持扩展
* - 依赖倒置:业务层依赖接口而非实现
*
* @author lzh
*/
public interface DispatchEngine {
/**
* 推荐执行人员(核心方法)
* <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 urgentContext 紧急任务的派单上下文
* @return 打断决策结果
*/
InterruptDecision evaluateInterrupt(Long currentAssigneeId, DispatchContext urgentContext);
/**
* 注册派单策略
* <p>
* 各业务线(保洁、安保、维修等)实现自己的派单策略,注册到引擎中
*
* @param strategy 派单策略实现
*/
void registerStrategy(DispatchStrategy strategy);
/**
* 注册业务类型与策略的映射
* <p>
* 建立业务类型CLEAN/REPAIR/SECURITY与策略名称的映射关系
*
* @param businessType 业务类型
* @param strategyName 策略名称
*/
void registerBusinessTypeStrategy(String businessType, String strategyName);
/**
* 根据业务类型获取策略
*
* @param businessType 业务类型
* @return 派单策略如果没有找到返回null
*/
DispatchStrategy getStrategyByBusinessType(String businessType);
}

View File

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

View File

@@ -0,0 +1,85 @@
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

@@ -0,0 +1,69 @@
package com.viewsh.module.ops.core.dispatch;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 打断决策结果
* 用于判断P0紧急任务是否可以打断当前正在执行的任务
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InterruptDecision {
/**
* 是否可以打断
*/
@Builder.Default
private boolean canInterrupt = false;
/**
* 打断原因
* 例如:"紧急任务优先级更高"
*/
private String reason;
/**
* 建议操作
* 例如:"暂停当前任务"、"等待当前任务完成"
*/
private String suggestion;
/**
* 可以打断
*/
public static InterruptDecision allow(String reason, String suggestion) {
return new InterruptDecision(true, reason, suggestion);
}
/**
* 不可以打断
*/
public static InterruptDecision deny(String reason, String suggestion) {
return new InterruptDecision(false, reason, suggestion);
}
/**
* 默认可以打断P0任务
*/
public static InterruptDecision allowByDefault() {
return InterruptDecision.allow(
"P0紧急任务优先级最高",
"建议暂停当前任务立即执行P0任务"
);
}
/**
* 别名方法,用于更流畅的调用
* Lombok 会生成 isCanInterrupt(),这里提供 canInterrupt() 别名
*/
public boolean canInterrupt() {
return isCanInterrupt();
}
}