fix(ops): 对账回填工牌 nickname,修复重启后派单人名降级为 deviceCode
根因:BadgeDeviceStatusSyncJob 硬编码 nickname=null,依赖 Redis 已有值。
重启后若 ops:badge:device:{deviceId} 的 nickname 丢失(TTL/清理/首次写入),
BadgeDeviceAreaAssignStrategy 会降级用 deviceCode,导致 assigneeName 变成 "43607737587"。
- SyncJob 注入 IotDeviceQueryApi,批量拉 IotDeviceSimpleRespDTO.nickname 做回填
- 状态一致但 Redis 缺 nickname 时也补写一次,覆盖最常见的重启路径
- AreaAssignStrategy 降级兜底改为 "工牌-尾号",避免再把裸 deviceCode 当人名暴露
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Long, String> 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 查询设备昵称
|
||||
* <p>
|
||||
* Redis 中 ops:badge:device:{deviceId} 的 nickname 字段可能因 TTL/重启/缓存清理而缺失,
|
||||
* 每次对账时以 IoT 为唯一可信源做回填,避免派单时降级为 deviceCode(如 "43607737587")。
|
||||
*/
|
||||
private Map<Long, String> loadDeviceNicknameMap(List<Long> deviceIds) {
|
||||
if (CollUtil.isEmpty(deviceIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
try {
|
||||
CommonResult<List<IotDeviceSimpleRespDTO>> result = iotDeviceQueryApi.batchGetDevices(deviceIds);
|
||||
if (!result.isSuccess() || CollUtil.isEmpty(result.getData())) {
|
||||
log.warn("[SyncJob] 查询设备昵称失败或为空: {}", result.getMsg());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步结果
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 解析执行人展示名称。
|
||||
* <p>
|
||||
* 优先用 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() : "未知工牌";
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择最佳设备
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user