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:
lzh
2026-02-25 17:59:54 +08:00
parent 161f55007b
commit 7dd3c9a5c4
4 changed files with 94 additions and 8 deletions

View File

@@ -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) {
}
}

View File

@@ -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);
// ==================== 区域管理 ====================
/**

View File

@@ -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) {

View File

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