fix(ops): 修复工牌设备状态残留 BUSY 导致下一工单无法派发的问题
- 新增 repairDeviceOrderConsistency 方法,检测设备关联的工单是否已终态,
若是则清除 currentOpsOrderId 并将设备状态恢复为 IDLE
- 定时对账 Job 增加工单一致性检查,自动修复历史残留
- 新增管理员手动修复 API:POST /ops/clean/order/repair-device-status
- 修复预存 bug:valueOf("busy") 改为 fromCode("busy") 避免 IllegalArgumentException
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,8 +64,8 @@ public class BadgeDeviceStatusSyncJob {
|
||||
public String execute() {
|
||||
try {
|
||||
SyncResult result = syncAllBadgeDeviceStatus();
|
||||
return StrUtil.format("工牌状态对账完成: 处理 {} 台,修正 {} 台,耗时 {} ms",
|
||||
result.syncCount, result.correctedCount, result.durationMs);
|
||||
return StrUtil.format("工牌状态对账完成: 处理 {} 台,修正 {} 台,工单修复 {} 台,耗时 {} ms",
|
||||
result.syncCount, result.correctedCount, result.orderRepairedCount, result.durationMs);
|
||||
} catch (Exception e) {
|
||||
log.error("[SyncJob] 全量对账失败", e);
|
||||
return StrUtil.format("工牌状态对账失败: {}", e.getMessage());
|
||||
@@ -84,6 +84,7 @@ public class BadgeDeviceStatusSyncJob {
|
||||
long startTime = System.currentTimeMillis();
|
||||
int syncCount = 0;
|
||||
int correctedCount = 0;
|
||||
int orderRepairedCount = 0;
|
||||
|
||||
// 1. 获取所有工牌设备关联关系
|
||||
List<OpsAreaDeviceRelationDO> badgeRelations = areaDeviceRelationMapper
|
||||
@@ -91,7 +92,7 @@ public class BadgeDeviceStatusSyncJob {
|
||||
|
||||
if (CollUtil.isEmpty(badgeRelations)) {
|
||||
log.info("[SyncJob] 无工牌设备,跳过对账");
|
||||
return new SyncResult(0, 0, System.currentTimeMillis() - startTime);
|
||||
return new SyncResult(0, 0, 0, System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
List<Long> deviceIds = badgeRelations.stream()
|
||||
@@ -117,6 +118,13 @@ public class BadgeDeviceStatusSyncJob {
|
||||
|
||||
// 4. 逐一对账并修正
|
||||
for (DeviceStatusRespDTO iotStatus : iotResult.getData()) {
|
||||
// 4a. 工单一致性检查(修复残留的已终态工单关联)
|
||||
boolean orderRepaired = badgeDeviceStatusService.repairDeviceOrderConsistency(iotStatus.getDeviceId());
|
||||
if (orderRepaired) {
|
||||
orderRepairedCount++;
|
||||
}
|
||||
|
||||
// 4b. IoT 在线/离线状态对账
|
||||
boolean corrected = syncSingleDevice(iotStatus, deviceAreaMap.get(iotStatus.getDeviceId()));
|
||||
syncCount++;
|
||||
if (corrected) {
|
||||
@@ -125,10 +133,10 @@ public class BadgeDeviceStatusSyncJob {
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.info("[SyncJob] 全量对账完成,共处理 {} 台设备,修正 {} 台,耗时 {} ms",
|
||||
syncCount, correctedCount, duration);
|
||||
log.info("[SyncJob] 全量对账完成,共处理 {} 台设备,修正 {} 台,工单修复 {} 台,耗时 {} ms",
|
||||
syncCount, correctedCount, orderRepairedCount, duration);
|
||||
|
||||
return new SyncResult(syncCount, correctedCount, duration);
|
||||
return new SyncResult(syncCount, correctedCount, orderRepairedCount, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,6 +197,6 @@ public class BadgeDeviceStatusSyncJob {
|
||||
/**
|
||||
* 同步结果
|
||||
*/
|
||||
public record SyncResult(int syncCount, int correctedCount, long durationMs) {
|
||||
public record SyncResult(int syncCount, int correctedCount, int orderRepairedCount, long durationMs) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,23 @@ public interface BadgeDeviceStatusService {
|
||||
*/
|
||||
List<BadgeDeviceStatusDTO> listBadgesWithCurrentOrder();
|
||||
|
||||
// ==================== 工单一致性修复 ====================
|
||||
|
||||
/**
|
||||
* 修复设备工单一致性
|
||||
* <p>
|
||||
* 检查设备关联的 currentOpsOrderId 对应的工单是否已终态(COMPLETED/CANCELLED),
|
||||
* 如果是,则清除工单关联并将设备状态设为 IDLE。
|
||||
* <p>
|
||||
* 适用场景:
|
||||
* - 历史手动完单未触发事件导致的状态残留
|
||||
* - 异常情况下工单已终态但设备状态未同步
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return true=进行了修复,false=无需修复
|
||||
*/
|
||||
boolean repairDeviceOrderConsistency(Long deviceId);
|
||||
|
||||
// ==================== 区域管理 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.viewsh.module.ops.environment.service.badge;
|
||||
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
|
||||
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||
import com.viewsh.module.ops.service.area.AreaDeviceService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -42,6 +45,9 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
|
||||
@Resource
|
||||
private AreaDeviceService areaDeviceService;
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
@Resource
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@@ -277,7 +283,7 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
|
||||
log.info("[updateBadgeOnlineStatus] 设备 {} 重新上线,但有进行中工单 {},保持状态 {}",
|
||||
deviceId, currentOrderIdObj, currentStatusStr);
|
||||
// 强制修正传入的状态为当前实际状态
|
||||
status = BadgeDeviceStatusEnum.valueOf(currentStatusStr);
|
||||
status = BadgeDeviceStatusEnum.fromCode(currentStatusStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,6 +470,48 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工单一致性修复 ====================
|
||||
|
||||
@Override
|
||||
public boolean repairDeviceOrderConsistency(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
BadgeDeviceStatusDTO deviceStatus = getBadgeStatus(deviceId);
|
||||
if (deviceStatus == null || deviceStatus.getCurrentOpsOrderId() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Long orderId = deviceStatus.getCurrentOpsOrderId();
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
|
||||
// 工单仍在进行中,无需修复
|
||||
if (order != null && !WorkOrderStatusEnum.fromStatus(order.getStatus()).isTerminal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 工单不存在或已终态 → 清除关联并恢复设备为 IDLE
|
||||
log.warn("[repairDeviceOrderConsistency] 清除残留工单关联: deviceId={}, orderId={}, orderStatus={}",
|
||||
deviceId, orderId, order != null ? order.getStatus() : "NOT_FOUND");
|
||||
|
||||
clearCurrentOrder(deviceId);
|
||||
|
||||
// 直接写 status 字段,避免 updateBadgeStatus 内部回写已清除的 currentOpsOrderId
|
||||
if (deviceStatus.getStatus() != null && deviceStatus.getStatus() != BadgeDeviceStatusEnum.IDLE) {
|
||||
String key = BADGE_STATUS_KEY_PREFIX + deviceId;
|
||||
redisTemplate.opsForHash().put(key, "status", BadgeDeviceStatusEnum.IDLE.getCode());
|
||||
redisTemplate.opsForHash().put(key, "statusChangeTime", LocalDateTime.now().toString());
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[repairDeviceOrderConsistency] 修复失败: deviceId={}", deviceId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBadgeArea(Long deviceId, Long areaId, String areaName) {
|
||||
if (deviceId == null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.viewsh.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.ManualCompleteOrderReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.UpgradePriorityReqDTO;
|
||||
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
|
||||
import com.viewsh.module.ops.environment.service.cleanorder.CleanWorkOrderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -33,6 +34,9 @@ public class CleanWorkOrderController {
|
||||
@Resource
|
||||
private CleanWorkOrderService cleanWorkOrderService;
|
||||
|
||||
@Resource
|
||||
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||
|
||||
@GetMapping("/timeline/{orderId}")
|
||||
@Operation(summary = "工单时间轴")
|
||||
@Parameter(name = "orderId", description = "工单ID", required = true)
|
||||
@@ -65,4 +69,13 @@ public class CleanWorkOrderController {
|
||||
cleanWorkOrderService.upgradePriority(req);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/repair-device-status")
|
||||
@Operation(summary = "修复工牌设备状态", description = "当设备状态残留为BUSY但关联工单已完成/取消时,修复设备状态为IDLE")
|
||||
@Parameter(name = "deviceId", description = "设备ID", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('ops:clean:order:complete')")
|
||||
public CommonResult<Boolean> repairDeviceStatus(@RequestParam("deviceId") Long deviceId) {
|
||||
boolean repaired = badgeDeviceStatusService.repairDeviceOrderConsistency(deviceId);
|
||||
return success(repaired);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user