diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/BadgeDeviceStatusSyncJob.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/BadgeDeviceStatusSyncJob.java index fdb052ef..c17c81f6 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/BadgeDeviceStatusSyncJob.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/BadgeDeviceStatusSyncJob.java @@ -4,7 +4,9 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import com.viewsh.framework.common.pojo.CommonResult; import com.viewsh.framework.tenant.core.job.TenantJob; +import com.viewsh.module.iot.api.device.IotDeviceQueryApi; import com.viewsh.module.iot.api.device.IotDeviceStatusQueryApi; +import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO; import com.viewsh.module.iot.api.device.dto.status.DeviceStatusRespDTO; import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO; import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO; @@ -18,6 +20,8 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -47,6 +51,9 @@ public class BadgeDeviceStatusSyncJob { @Resource private IotDeviceStatusQueryApi iotDeviceStatusQueryApi; + @Resource + private IotDeviceQueryApi iotDeviceQueryApi; + @Resource private OpsAreaDeviceRelationMapper areaDeviceRelationMapper; @@ -120,6 +127,9 @@ public class BadgeDeviceStatusSyncJob { OpsAreaDeviceRelationDO::getAreaId, (existing, replacement) -> existing)); + // 3b. 批量查询设备 nickname(IoT 是唯一可信源),防止 Redis key 丢失后降级到 deviceCode + Map deviceNicknameMap = loadDeviceNicknameMap(deviceIds); + // 4. 逐一对账并修正 for (DeviceStatusRespDTO iotStatus : iotResult.getData()) { // 4a. 工单一致性检查(修复残留的已终态工单关联) @@ -135,7 +145,10 @@ public class BadgeDeviceStatusSyncJob { } // 4b. IoT 在线/离线状态对账 - boolean corrected = syncSingleDevice(iotStatus, deviceAreaMap.get(iotStatus.getDeviceId())); + boolean corrected = syncSingleDevice( + iotStatus, + deviceAreaMap.get(iotStatus.getDeviceId()), + deviceNicknameMap.get(iotStatus.getDeviceId())); syncCount++; if (corrected) { correctedCount++; @@ -154,9 +167,10 @@ public class BadgeDeviceStatusSyncJob { * * @param iotStatus IoT 设备状态 * @param areaId 设备所属区域ID + * @param nickname 设备昵称(从 IoT 查到的权威值,允许 null) * @return 是否进行了修正 */ - private boolean syncSingleDevice(DeviceStatusRespDTO iotStatus, Long areaId) { + private boolean syncSingleDevice(DeviceStatusRespDTO iotStatus, Long areaId, String nickname) { Long deviceId = iotStatus.getDeviceId(); try { @@ -168,8 +182,20 @@ public class BadgeDeviceStatusSyncJob { boolean opsOnline = opsStatus != null && opsStatus.getStatus() != null && opsStatus.getStatus().isActive(); - // 如果状态一致,无需修正 + // 如果状态一致,但 Redis 缺 nickname 而 IoT 有值,则补写一次防止派单时降级显示 deviceCode if (iotOnline == opsOnline) { + if (iotOnline && nickname != null + && (opsStatus == null || opsStatus.getNickname() == null)) { + badgeDeviceStatusService.updateBadgeOnlineStatus( + deviceId, + iotStatus.getDeviceCode(), + nickname, + areaId, + BadgeDeviceStatusEnum.IDLE, + "定时对账补写-昵称"); + log.info("[SyncJob] 补写设备昵称:deviceId={}, nickname={}", deviceId, nickname); + return true; + } return false; } @@ -178,17 +204,17 @@ public class BadgeDeviceStatusSyncJob { badgeDeviceStatusService.updateBadgeOnlineStatus( deviceId, iotStatus.getDeviceCode(), - null, // nickname: 对账场景不更新昵称,保留Redis中已有值 + nickname, areaId, BadgeDeviceStatusEnum.IDLE, "定时对账修正-上线"); - log.info("[SyncJob] 修正设备状态:deviceId={}, IoT=ONLINE, Ops={}", - deviceId, opsOnline ? "ACTIVE" : "OFFLINE/NULL"); + log.info("[SyncJob] 修正设备状态:deviceId={}, IoT=ONLINE, Ops={}, nickname={}", + deviceId, opsOnline ? "ACTIVE" : "OFFLINE/NULL", nickname); } else { badgeDeviceStatusService.updateBadgeOnlineStatus( deviceId, iotStatus.getDeviceCode(), - null, // nickname: 对账场景不更新昵称,保留Redis中已有值 + nickname, null, BadgeDeviceStatusEnum.OFFLINE, "定时对账修正-离线"); @@ -204,6 +230,35 @@ public class BadgeDeviceStatusSyncJob { } } + /** + * 批量从 IoT 查询设备昵称 + *

+ * Redis 中 ops:badge:device:{deviceId} 的 nickname 字段可能因 TTL/重启/缓存清理而缺失, + * 每次对账时以 IoT 为唯一可信源做回填,避免派单时降级为 deviceCode(如 "43607737587")。 + */ + private Map loadDeviceNicknameMap(List deviceIds) { + if (CollUtil.isEmpty(deviceIds)) { + return Collections.emptyMap(); + } + try { + CommonResult> result = iotDeviceQueryApi.batchGetDevices(deviceIds); + if (!result.isSuccess() || CollUtil.isEmpty(result.getData())) { + log.warn("[SyncJob] 查询设备昵称失败或为空: {}", result.getMsg()); + return Collections.emptyMap(); + } + Map map = new HashMap<>(result.getData().size()); + for (IotDeviceSimpleRespDTO dto : result.getData()) { + if (dto.getId() != null && dto.getNickname() != null) { + map.put(dto.getId(), dto.getNickname()); + } + } + return map; + } catch (Exception e) { + log.warn("[SyncJob] 批量查询设备昵称异常,本次对账跳过昵称回填", e); + return Collections.emptyMap(); + } + } + /** * 同步结果 */ diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java index b1923737..c3f9cfd0 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceAreaAssignStrategy.java @@ -89,8 +89,7 @@ public class BadgeDeviceAreaAssignStrategy implements AssignStrategy { if (selectedDevice != null) { String reason = buildRecommendationReason(selectedDevice, context); - String assigneeName = selectedDevice.getNickname() != null - ? selectedDevice.getNickname() : selectedDevice.getDeviceCode(); + String assigneeName = resolveAssigneeName(selectedDevice); return AssigneeRecommendation.of( selectedDevice.getDeviceId(), assigneeName, @@ -118,8 +117,7 @@ public class BadgeDeviceAreaAssignStrategy implements AssignStrategy { .map(device -> { int score = calculateScore(device); String reason = buildRecommendationReason(device, context); - String assigneeName = device.getNickname() != null - ? device.getNickname() : device.getDeviceCode(); + String assigneeName = resolveAssigneeName(device); return AssigneeRecommendation.of( device.getDeviceId(), assigneeName, @@ -133,6 +131,25 @@ public class BadgeDeviceAreaAssignStrategy implements AssignStrategy { // ==================== 私有方法 ==================== + /** + * 解析执行人展示名称。 + *

+ * 优先用 nickname;nickname 缺失时(例如 Redis 状态缓存被清理、IoT 侧未维护昵称), + * 返回 "工牌-尾号" 这样的可读降级文案,避免把 deviceCode/IMEI 这类长数字串直接当作人员名字暴露给调用方。 + */ + private String resolveAssigneeName(BadgeDeviceStatusDTO device) { + String nickname = device.getNickname(); + if (nickname != null && !nickname.isBlank()) { + return nickname; + } + String code = device.getDeviceCode(); + if (code != null && !code.isBlank()) { + int len = code.length(); + return "工牌-" + (len > 4 ? code.substring(len - 4) : code); + } + return device.getDeviceId() != null ? "工牌-" + device.getDeviceId() : "未知工牌"; + } + /** * 选择最佳设备 */