From 56a40edd8e99358629cff243891f8bd2b22defe5 Mon Sep 17 00:00:00 2001 From: 2995840376 <2995840376@xiaomi.com> Date: Tue, 27 Oct 2026 18:40:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(G):=20=E8=AE=BE=E5=A4=87=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E4=B8=9A=E5=8A=A1=E5=BC=82=E5=B8=B8=E9=99=8D?= =?UTF-8?q?=E5=99=AA,=20=E9=81=BF=E5=85=8D=E5=AE=9A=E6=97=B6=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E5=88=B7=E7=88=86=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:定时场景(SceneCronScheduler, 如 bb 场景)每分钟执行 7 条规则并向涂鸦设备下发控制指令, 而设备归属用户尚未绑定涂鸦账号,每次下发都抛 BusinessException,并以 ERROR + 完整堆栈记录, 单日产生上千条错误日志,掩盖真实故障。 改动(iot-device DeviceCommandListener): - 区分业务可预期异常与系统异常: BusinessException(如"请先在个人中心绑定涂鸦账号")改为 WARN 单行输出,不再打印堆栈 - 新增 60 秒日志抑制窗口:同一 deviceId 在窗口内只详细记录一次,其余静默累计; 下一个窗口首次记录时汇总上一周期被抑制的次数,避免高频场景刷屏 - 系统异常保留 ERROR + 堆栈(窗口内首次),同样受窗口抑制保护 生产验证:重启后日志变为 WARN「设备控制指令业务异常: 请先在个人中心绑定涂鸦账号(上一周期同类异常已抑制 N 次)」, 不再输出堆栈;RabbitMQ 队列深度为 0;4 个服务 active。 --- .../device/event/DeviceCommandListener.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/backend/iot-device/src/main/java/com/iot/device/event/DeviceCommandListener.java b/backend/iot-device/src/main/java/com/iot/device/event/DeviceCommandListener.java index 76b87d1..0e2c980 100644 --- a/backend/iot-device/src/main/java/com/iot/device/event/DeviceCommandListener.java +++ b/backend/iot-device/src/main/java/com/iot/device/event/DeviceCommandListener.java @@ -1,5 +1,6 @@ package com.iot.device.event; +import com.iot.common.exception.BusinessException; import com.iot.device.config.RabbitConfig; import com.iot.device.entity.Device; import com.iot.device.mapper.DeviceMapper; @@ -12,6 +13,7 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * 设备控制指令监听器 @@ -23,6 +25,12 @@ import java.util.Map; @RequiredArgsConstructor public class DeviceCommandListener { + /** 日志抑制窗口:同一 key 在该窗口内只详细记录一次,其余静默累计 */ + private static final long LOG_SUPPRESS_WINDOW_MS = 60_000L; + + /** key -> {窗口起始时间, 窗口内被抑制次数, 上一窗口被抑制次数} */ + private final Map logThrottle = new ConcurrentHashMap<>(); + private final DeviceMapper deviceMapper; private final TuyaCloudService tuyaCloudService; @@ -90,8 +98,52 @@ public class DeviceCommandListener { } else { log.info("平台[{}]控制指令暂未适配: deviceId={}", platform, deviceId); } + } catch (BusinessException be) { + // 业务可预期异常(如用户尚未绑定涂鸦账号):属于账号/数据状态问题而非系统故障, + // 不打印堆栈,并按 60 秒窗口抑制,避免定时场景高频触发时刷爆错误日志 + logThrottled("biz:" + message.get("deviceId"), true, "设备控制指令业务异常", be); } catch (Exception e) { - log.error("执行设备控制指令失败: {}", message, e); + logThrottled("err:" + message.get("deviceId"), false, "执行设备控制指令失败", e); + } + } + + /** + * 受控日志输出:同一 key 在 60 秒窗口内只详细记录一次,其余静默累计; + * 下一个窗口首次记录时,汇总上一窗口被抑制的次数。 + * + * @param business true=业务可预期异常(WARN 单行,不打堆栈);false=系统异常(ERROR + 堆栈) + */ + private void logThrottled(String key, boolean business, String message, Throwable t) { + long now = System.currentTimeMillis(); + long[] slot = logThrottle.compute(key, (k, v) -> { + if (v == null) { + return new long[]{now, 0, 0}; + } + if (now - v[0] >= LOG_SUPPRESS_WINDOW_MS) { + // 新窗口开启:继承上一窗口被抑制的次数,用于汇总输出 + return new long[]{now, 0, v[1]}; + } + v[1]++; + return v; + }); + if (slot[1] != 0) { + // 本窗口内已详细记录过一次,静默累计即可 + return; + } + long suppressed = slot[2]; + slot[2] = 0; + if (business) { + if (suppressed > 0) { + log.warn("{}: {}(上一周期同类异常已抑制 {} 次)", message, t.getMessage(), suppressed); + } else { + log.warn("{}: {}", message, t.getMessage()); + } + } else { + if (suppressed > 0) { + log.error("{}(上一周期同类异常已抑制 {} 次): {}", message, suppressed, t.getMessage(), t); + } else { + log.error("{}: {}", message, t.getMessage(), t); + } } } }