diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/redis/TrafficActiveOrderRedisDAO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/redis/TrafficActiveOrderRedisDAO.java index 3b473e2..85a366e 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/redis/TrafficActiveOrderRedisDAO.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/dal/redis/TrafficActiveOrderRedisDAO.java @@ -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); } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java index 723860c..8944d33 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java @@ -160,6 +160,34 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { } } + // 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(); diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/TrafficActiveOrderInitializer.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/TrafficActiveOrderInitializer.java new file mode 100644 index 0000000..40e77c9 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/job/TrafficActiveOrderInitializer.java @@ -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 启动校准器 + *

+ * 职责:服务启动时,扫描所有 ops:clean:traffic:active-order:* key, + * 逐个与 DB 比对,清理已终态(COMPLETED/CANCELLED)或已删除的残留标记。 + *

+ * 解决场景: + * - 服务重启期间 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 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 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); + } +}