fix(ops): 调度状态补偿 QUEUED→终态跳过 DISPATCHED 场景
UserDispatchStatusEventListener: - assigneeId 兜底从工单主表获取(forceComplete 等 payload 缺失场景) - QUEUED→COMPLETED/CANCELLED 补偿 decrementWaitingTaskCount UserDispatchStatusServiceImpl: - 新增 LUA_DECREMENT_WAITING 脚本,安全递减 waitingTaskCount(不低于 0) OpsOrderEventService:新增 8 参数 recordEvent 重载(含 operatorName) DispatchEngineServiceAdapter:reason 文案统一为"手动派单" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,7 +125,7 @@ public class DispatchEngineServiceAdapter implements DispatchEngineService {
|
|||||||
.queueId(queueId)
|
.queueId(queueId)
|
||||||
.operatorType(OperatorTypeEnum.ADMIN)
|
.operatorType(OperatorTypeEnum.ADMIN)
|
||||||
.operatorId(assigneeId)
|
.operatorId(assigneeId)
|
||||||
.reason("管理员手动派单")
|
.reason("手动派单")
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.viewsh.module.ops.service.dispatch;
|
package com.viewsh.module.ops.service.dispatch;
|
||||||
|
|
||||||
import com.viewsh.module.ops.core.event.OrderStateChangedEvent;
|
import com.viewsh.module.ops.core.event.OrderStateChangedEvent;
|
||||||
|
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||||
|
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||||
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -28,6 +30,9 @@ public class UserDispatchStatusEventListener {
|
|||||||
@Resource
|
@Resource
|
||||||
private UserDispatchStatusService userDispatchStatusService;
|
private UserDispatchStatusService userDispatchStatusService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OpsOrderMapper opsOrderMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听工单状态变更,同步更新人员调度状态
|
* 监听工单状态变更,同步更新人员调度状态
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +44,13 @@ public class UserDispatchStatusEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Long assigneeId = event.getPayloadLong("assigneeId");
|
Long assigneeId = event.getPayloadLong("assigneeId");
|
||||||
|
// payload 中没有 assigneeId 时,从工单主表兜底(forceComplete 等场景)
|
||||||
|
if (assigneeId == null) {
|
||||||
|
OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId());
|
||||||
|
if (order != null) {
|
||||||
|
assigneeId = order.getAssigneeId();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (assigneeId == null) {
|
if (assigneeId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,12 +68,27 @@ public class UserDispatchStatusEventListener {
|
|||||||
case QUEUED -> userDispatchStatusService.onOrderQueued(assigneeId, orderId, orderType);
|
case QUEUED -> userDispatchStatusService.onOrderQueued(assigneeId, orderId, orderType);
|
||||||
case CONFIRMED, ARRIVED -> userDispatchStatusService.onOrderStatusAdvanced(
|
case CONFIRMED, ARRIVED -> userDispatchStatusService.onOrderStatusAdvanced(
|
||||||
assigneeId, orderId, newStatus.getStatus());
|
assigneeId, orderId, newStatus.getStatus());
|
||||||
case COMPLETED, CANCELLED -> userDispatchStatusService.onOrderFinished(assigneeId, orderId);
|
case COMPLETED, CANCELLED -> handleFinished(assigneeId, orderId, oldStatus);
|
||||||
case PAUSED -> userDispatchStatusService.onOrderPaused(assigneeId, orderId);
|
case PAUSED -> userDispatchStatusService.onOrderPaused(assigneeId, orderId);
|
||||||
default -> log.debug("人员调度状态无需处理: orderId={}, status={}", orderId, newStatus);
|
default -> log.debug("人员调度状态无需处理: orderId={}, status={}", orderId, newStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 COMPLETED/CANCELLED 状态:
|
||||||
|
* 如果是从 QUEUED 直接到终态(forceComplete / admin cancel),
|
||||||
|
* 需要额外减 waitingTaskCount,因为跳过了 DISPATCHED 阶段。
|
||||||
|
*/
|
||||||
|
private void handleFinished(Long assigneeId, Long orderId, WorkOrderStatusEnum oldStatus) {
|
||||||
|
userDispatchStatusService.onOrderFinished(assigneeId, orderId);
|
||||||
|
|
||||||
|
// QUEUED→COMPLETED/CANCELLED 跳过了 DISPATCHED,waitingTaskCount 没有被减
|
||||||
|
if (oldStatus == WorkOrderStatusEnum.QUEUED) {
|
||||||
|
userDispatchStatusService.decrementWaitingTaskCount(assigneeId);
|
||||||
|
log.info("QUEUED 直接完成/取消,补偿减少 waitingTaskCount: assigneeId={}, orderId={}", assigneeId, orderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 DISPATCHED 状态:区分直接派发 vs 从排队派发
|
* 处理 DISPATCHED 状态:区分直接派发 vs 从排队派发
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -101,6 +101,17 @@ public interface UserDispatchStatusService {
|
|||||||
*/
|
*/
|
||||||
void onOrderResumed(Long userId, Long orderId);
|
void onOrderResumed(Long userId, Long orderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补偿减少 waitingTaskCount
|
||||||
|
* <p>
|
||||||
|
* 当工单从 QUEUED 直接到 COMPLETED/CANCELLED(跳过 DISPATCHED)时,
|
||||||
|
* waitingTaskCount 在入队时+1 但没有经过 onQueuedOrderDispatched 减回,
|
||||||
|
* 需要通过此方法补偿。
|
||||||
|
*
|
||||||
|
* @param userId 执行人ID
|
||||||
|
*/
|
||||||
|
void decrementWaitingTaskCount(Long userId);
|
||||||
|
|
||||||
// ==================== 读操作(调度时使用)====================
|
// ==================== 读操作(调度时使用)====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public class UserDispatchStatusServiceImpl implements UserDispatchStatusService
|
|||||||
private DefaultRedisScript<Long> queuedToDispatchedScript;
|
private DefaultRedisScript<Long> queuedToDispatchedScript;
|
||||||
private DefaultRedisScript<Long> pauseResumeScript;
|
private DefaultRedisScript<Long> pauseResumeScript;
|
||||||
private DefaultRedisScript<Long> statusAdvancedScript;
|
private DefaultRedisScript<Long> statusAdvancedScript;
|
||||||
|
private DefaultRedisScript<Long> decrementWaitingScript;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
@@ -48,6 +49,7 @@ public class UserDispatchStatusServiceImpl implements UserDispatchStatusService
|
|||||||
queuedToDispatchedScript = buildScript(LUA_QUEUED_TO_DISPATCHED);
|
queuedToDispatchedScript = buildScript(LUA_QUEUED_TO_DISPATCHED);
|
||||||
pauseResumeScript = buildScript(LUA_PAUSE_RESUME);
|
pauseResumeScript = buildScript(LUA_PAUSE_RESUME);
|
||||||
statusAdvancedScript = buildScript(LUA_STATUS_ADVANCED);
|
statusAdvancedScript = buildScript(LUA_STATUS_ADVANCED);
|
||||||
|
decrementWaitingScript = buildScript(LUA_DECREMENT_WAITING);
|
||||||
log.info("人员调度状态服务已初始化,Lua 脚本已预加载");
|
log.info("人员调度状态服务已初始化,Lua 脚本已预加载");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +173,21 @@ public class UserDispatchStatusServiceImpl implements UserDispatchStatusService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void decrementWaitingTaskCount(Long userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.execute(decrementWaitingScript,
|
||||||
|
List.of(keyOf(userId)),
|
||||||
|
String.valueOf(System.currentTimeMillis()));
|
||||||
|
log.debug("人员状态更新[DECREMENT_WAITING]: userId={}", userId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("人员状态更新[DECREMENT_WAITING]失败: userId={}", userId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 读操作 ====================
|
// ==================== 读操作 ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -389,4 +406,21 @@ public class UserDispatchStatusServiceImpl implements UserDispatchStatusService
|
|||||||
redis.call('EXPIRE', KEYS[1], 86400)
|
redis.call('EXPIRE', KEYS[1], 86400)
|
||||||
return 1
|
return 1
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DECREMENT_WAITING: 补偿减少 waitingTaskCount
|
||||||
|
* 用于 QUEUED→COMPLETED/CANCELLED 跳过 DISPATCHED 的场景
|
||||||
|
* KEYS[1] = hash key
|
||||||
|
* ARGV[1] = timestamp
|
||||||
|
*/
|
||||||
|
private static final String LUA_DECREMENT_WAITING = """
|
||||||
|
local waiting = redis.call('HINCRBY', KEYS[1], 'waitingTaskCount', -1)
|
||||||
|
if waiting < 0 then
|
||||||
|
waiting = 0
|
||||||
|
redis.call('HSET', KEYS[1], 'waitingTaskCount', '0')
|
||||||
|
end
|
||||||
|
redis.call('HSET', KEYS[1], 'lastUpdateTime', ARGV[1])
|
||||||
|
redis.call('EXPIRE', KEYS[1], 86400)
|
||||||
|
return waiting
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,33 @@ public class OpsOrderEventService {
|
|||||||
opsOrderId, fromStatus, toStatus, eventType);
|
opsOrderId, fromStatus, toStatus, eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录工单事件(含操作人姓名冗余)
|
||||||
|
*/
|
||||||
|
public void recordEvent(Long opsOrderId,
|
||||||
|
String fromStatus,
|
||||||
|
String toStatus,
|
||||||
|
String eventType,
|
||||||
|
String operatorType,
|
||||||
|
Long operatorId,
|
||||||
|
String operatorName,
|
||||||
|
String remark) {
|
||||||
|
OpsOrderEventDO event = OpsOrderEventDO.builder()
|
||||||
|
.opsOrderId(opsOrderId)
|
||||||
|
.fromStatus(fromStatus)
|
||||||
|
.toStatus(toStatus)
|
||||||
|
.eventType(eventType)
|
||||||
|
.operatorType(operatorType)
|
||||||
|
.operatorId(operatorId)
|
||||||
|
.operatorName(operatorName)
|
||||||
|
.eventTime(LocalDateTime.now())
|
||||||
|
.remark(remark)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
eventMapper.insert(event);
|
||||||
|
|
||||||
|
log.debug("记录工单事件: orderId={}, {} -> {}, eventType={}, operatorName={}",
|
||||||
|
opsOrderId, fromStatus, toStatus, eventType, operatorName);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user