fix(clean): 补齐客流活跃工单缓存自愈逻辑

- 为客流活跃工单 Redis 标记补充 TTL,避免长期残留\n- 创建工单前命中 Redis 时回查 DB,自动清理终态脏数据并刷新过期状态\n- 新增启动校准器,服务启动时批量清理或刷新 area 级活跃工单缓存
This commit is contained in:
lzh
2026-03-31 22:57:28 +08:00
parent d3eecc63ef
commit f0fa5f1c46
3 changed files with 146 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.util.Map;
/**
@@ -23,6 +24,7 @@ import java.util.Map;
public class TrafficActiveOrderRedisDAO {
private static final String KEY_PATTERN = "ops:clean:traffic:active-order:%s";
private static final Duration ACTIVE_ORDER_TTL = Duration.ofHours(24);
private static final String FIELD_ORDER_ID = "orderId";
private static final String FIELD_STATUS = "status";
@@ -41,6 +43,7 @@ public class TrafficActiveOrderRedisDAO {
FIELD_STATUS, status,
FIELD_PRIORITY, String.valueOf(priority)
));
stringRedisTemplate.expire(key, ACTIVE_ORDER_TTL);
log.debug("[TrafficActiveOrderRedisDAO] 标记活跃工单: areaId={}, orderId={}, status={}, priority={}",
areaId, orderId, status, priority);
}
@@ -76,6 +79,7 @@ public class TrafficActiveOrderRedisDAO {
String key = buildKey(areaId);
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(key))) {
stringRedisTemplate.opsForHash().put(key, FIELD_STATUS, newStatus);
stringRedisTemplate.expire(key, ACTIVE_ORDER_TTL);
log.debug("[TrafficActiveOrderRedisDAO] 更新状态: areaId={}, newStatus={}", areaId, newStatus);
}
}
@@ -87,6 +91,7 @@ public class TrafficActiveOrderRedisDAO {
String key = buildKey(areaId);
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(key))) {
stringRedisTemplate.opsForHash().put(key, FIELD_PRIORITY, String.valueOf(newPriority));
stringRedisTemplate.expire(key, ACTIVE_ORDER_TTL);
log.debug("[TrafficActiveOrderRedisDAO] 更新优先级: areaId={}, newPriority={}", areaId, newPriority);
}
}

View File

@@ -160,6 +160,34 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
}
}
// 1.5 Redis 命中时,回查 DB 校验真实状态(防止缓存与 DB 不一致)
if (activeOrder != null) {
OpsOrderDO dbOrder = opsOrderMapper.selectById(activeOrder.getOrderId());
if (dbOrder == null
|| WorkOrderStatusEnum.COMPLETED.getStatus().equals(dbOrder.getStatus())
|| WorkOrderStatusEnum.CANCELLED.getStatus().equals(dbOrder.getStatus())) {
// 工单已终态或已删除Redis 标记过期,清除
log.warn("[CleanOrderCreateEventHandler] Redis活跃标记已过期(DB状态:{}),清除: areaId={}, orderId={}",
dbOrder != null ? dbOrder.getStatus() : "NOT_FOUND", areaId, activeOrder.getOrderId());
trafficActiveOrderRedisDAO.removeActive(areaId);
activeOrder = null;
} else if (!activeOrder.getStatus().equals(dbOrder.getStatus())
|| !activeOrder.getPriority().equals(dbOrder.getPriority())) {
// Redis 状态/优先级过期,用 DB 真实值刷新
log.info("[CleanOrderCreateEventHandler] Redis状态过期已刷新: areaId={}, orderId={}, redis={}/{}, db={}/{}",
areaId, activeOrder.getOrderId(),
activeOrder.getStatus(), activeOrder.getPriority(),
dbOrder.getStatus(), dbOrder.getPriority());
activeOrder = ActiveOrderInfo.builder()
.orderId(dbOrder.getId())
.status(dbOrder.getStatus())
.priority(dbOrder.getPriority())
.build();
trafficActiveOrderRedisDAO.markActive(areaId, dbOrder.getId(),
dbOrder.getStatus(), dbOrder.getPriority());
}
}
// 2. 有活跃工单 → 升级或忽略
if (activeOrder != null) {
String status = activeOrder.getStatus();

View File

@@ -0,0 +1,113 @@
package com.viewsh.module.ops.environment.job;
import com.viewsh.framework.tenant.core.util.TenantUtils;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.environment.dal.redis.TrafficActiveOrderRedisDAO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
/**
* 客流活跃工单 Redis 启动校准器
* <p>
* 职责:服务启动时,扫描所有 ops:clean:traffic:active-order:* key
* 逐个与 DB 比对清理已终态COMPLETED/CANCELLED或已删除的残留标记。
* <p>
* 解决场景:
* - 服务重启期间 Spring Event 丢失,导致 removeActive 未执行
* - 异常导致 clearTrafficActiveOrder 被跳过
*
* @author AI
*/
@Slf4j
@Component
public class TrafficActiveOrderInitializer implements ApplicationRunner {
private static final String KEY_PREFIX = "ops:clean:traffic:active-order:";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private TrafficActiveOrderRedisDAO trafficActiveOrderRedisDAO;
@Override
public void run(ApplicationArguments args) {
log.info("[初始化] 开始校准客流活跃工单 Redis 标记...");
try {
TenantUtils.executeIgnore(this::calibrate);
} catch (Exception e) {
log.error("[初始化] 客流活跃工单 Redis 校准失败", e);
}
}
private void calibrate() {
// SCAN 匹配所有活跃工单 key
Set<String> keys = stringRedisTemplate.keys(KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
log.info("[初始化] 无客流活跃工单 Redis 标记,跳过校准");
return;
}
int total = keys.size();
int cleaned = 0;
int refreshed = 0;
for (String key : keys) {
try {
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);
String orderIdStr = (String) entries.get("orderId");
if (orderIdStr == null) {
stringRedisTemplate.delete(key);
cleaned++;
continue;
}
Long orderId = Long.parseLong(orderIdStr);
OpsOrderDO dbOrder = opsOrderMapper.selectById(orderId);
// 提取 areaId from key: ops:clean:traffic:active-order:{areaId}
String areaIdStr = key.substring(KEY_PREFIX.length());
Long areaId = Long.parseLong(areaIdStr);
if (dbOrder == null
|| "COMPLETED".equals(dbOrder.getStatus())
|| "CANCELLED".equals(dbOrder.getStatus())) {
// 工单已终态或不存在,清除残留标记
trafficActiveOrderRedisDAO.removeActive(areaId);
log.info("[初始化] 清除过期活跃标记: areaId={}, orderId={}, dbStatus={}",
areaId, orderId, dbOrder != null ? dbOrder.getStatus() : "NOT_FOUND");
cleaned++;
} else {
// 工单仍活跃,用 DB 真实状态刷新 Redis
String cachedStatus = (String) entries.get("status");
String cachedPriority = (String) entries.get("priority");
if (!dbOrder.getStatus().equals(cachedStatus)
|| !String.valueOf(dbOrder.getPriority()).equals(cachedPriority)) {
trafficActiveOrderRedisDAO.markActive(areaId, dbOrder.getId(),
dbOrder.getStatus(), dbOrder.getPriority());
log.info("[初始化] 刷新活跃标记状态: areaId={}, orderId={}, {}→{}",
areaId, orderId, cachedStatus, dbOrder.getStatus());
refreshed++;
}
}
} catch (Exception e) {
log.warn("[初始化] 校准单个 key 失败: key={}", key, e);
}
}
log.info("[初始化] 客流活跃工单 Redis 校准完成:总计 {} 个,清除 {} 个,刷新 {} 个",
total, cleaned, refreshed);
}
}