From fe9bba84f5ded2ff0efec46cc706149b1a4073d8 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 14 十月 2026 13:12:02 +0800
Subject: [PATCH] 添加日志记录

---
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/IotCommandLog.java         |  157 +++++++++++++++++++++++++++++++++++++++
 ruoyi-service/ruoyi-integration/src/main/resources/logback.xml                                                   |   26 ++++++
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java |   29 +++++--
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/produce/IotMessageProduce.java   |    5 +
 4 files changed, 209 insertions(+), 8 deletions(-)

diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
index b4a3c23..3038896 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
@@ -17,6 +17,7 @@
 import com.ruoyi.integration.iotda.model.*;
 import com.ruoyi.integration.iotda.utils.produce.IotMessageProduce;
 import com.ruoyi.integration.iotda.utils.tools.CP56Time2aConverter;
+import com.ruoyi.integration.iotda.utils.tools.IotCommandLog;
 import com.ruoyi.integration.iotda.utils.tools.MessageUtil;
 import com.ruoyi.integration.iotda.utils.tools.StrategyUtil;
 import com.ruoyi.integration.iotda.utils.tools.ChargingMessageUtil;
@@ -84,11 +85,12 @@
             content = body.getJSONObject("content");
         }catch (Exception e){
             String content1 = body.getString("content");
+            // 按对象解析失败时无法识别 service_id(无法判断是否心跳),记录原始报文兜底便于排查
+            IotCommandLog.uplinkUnparsed(deviceNoOf(jsonObject), content1);
             String substring = content1.substring(content1.length() - 1);
             if(!"}".equals(substring)){
                 content1 = content1.substring(0, content1.length() - 1) + "}";
             }
-//            log.info("content1:"+content1);
             content = JSON.parseObject(content1);
         }
         JSONObject header = jsonObject.getJSONObject("notify_data").getJSONObject("header");
@@ -96,10 +98,8 @@
         String nodeId = header.getString("node_id"); // 设备编号
         String productId = header.getString("product_id"); // 产品id
         String service_id = content.getString("service_id");
-        if(!SendTagConstant.PING.equals(service_id)){
-            log.info("收到IOT发送的消息:{}",jsonObject.toJSONString());
-        }
-//        log.info("服务id:{}",service_id);
+        // 记录上行指令日志(独立日志文件、含指令中文含义;心跳 ping 不记录)
+        IotCommandLog.uplink(deviceNoOf(jsonObject), service_id, content.toJSONString());
         ChargingMessage chargingMessage = new ChargingMessage();
         chargingMessage.setServiceId(service_id);
         // 设备消息下发
@@ -127,7 +127,6 @@
                 log.warn("ONLINE消息处理:{} 毫秒", System.currentTimeMillis() - ONLINE);
                 break;
             case SendTagConstant.PING:
-                long PING = System.currentTimeMillis();
                 PingMessage pingMessage = JSON.parseObject(content.toJSONString(),PingMessage.class);
                 String fullName = pingMessage.getCharging_pile_code() + pingMessage.getCharging_gun_code();
                 //主动丢弃一次心跳数据,用于降低调华为接口评率,防止被限流
@@ -174,8 +173,6 @@
                 vo1.setPile_code(pingMessage.getCharging_pile_code());
                 vo1.setStatus(pingMessage.getCharging_gun_status());
                 chargingPileClient.updateChargingPileStatus(vo1);
-                
-                log.warn("PING消息处理:{} 毫秒", System.currentTimeMillis() - PING);
                 break;
             case SendTagConstant.END_CHARGE:
                 long END_CHARGE = System.currentTimeMillis();
@@ -531,6 +528,22 @@
         return AjaxResult.success();
     }
 
+    /**
+     * 从IoTDA推送报文中提取设备编号(node_id 优先,缺失时回退 device_id)
+     */
+    private String deviceNoOf(JSONObject msg) {
+        try {
+            JSONObject header = msg.getJSONObject("notify_data").getJSONObject("header");
+            if (header == null) {
+                return null;
+            }
+            String nodeId = header.getString("node_id");
+            return (nodeId == null || nodeId.isEmpty()) ? header.getString("device_id") : nodeId;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
 
     @ApiOperation(value = "运营平台远程控制启机",tags = {"硬件接口"})
     @GetMapping(value = "/platformStartCharging")
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/produce/IotMessageProduce.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/produce/IotMessageProduce.java
index 4ca0ffb..5e4513e 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/produce/IotMessageProduce.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/produce/IotMessageProduce.java
@@ -7,6 +7,7 @@
 import com.huaweicloud.sdk.iotda.v5.model.DeviceMessageRequest;
 import com.ruoyi.common.core.utils.uuid.UUID;
 import com.ruoyi.integration.iotda.builder.IotBuilder;
+import com.ruoyi.integration.iotda.utils.tools.IotCommandLog;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -38,11 +39,15 @@
         body.withName(name);
         body.withMessage(message);
         request.withBody(body);
+        // 记录下行指令日志(独立日志文件、含指令中文含义;心跳应答 pong 不记录)
+        String serviceId = message == null ? null : message.getString("service_id");
+        IotCommandLog.downlink(code, name, serviceId, message);
         try {
             CreateMessageResponse response = iotBuilder.buildIot().createMessage(request);
             return JSON.toJSONString(response);
         } catch (Exception e) {
             e.printStackTrace();
+            IotCommandLog.downlinkFail(code, name, serviceId, message, e.getMessage());
             return e.getMessage();
         }
     }
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/IotCommandLog.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/IotCommandLog.java
new file mode 100644
index 0000000..cb4a52e
--- /dev/null
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/IotCommandLog.java
@@ -0,0 +1,157 @@
+package com.ruoyi.integration.iotda.utils.tools;
+
+import com.ruoyi.integration.iotda.constant.SendTagConstant;
+import com.ruoyi.integration.iotda.enums.ServiceIdMenu;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 充电桩指令收发日志
+ * <p>
+ * 1、使用独立 logger(pile-command)输出到独立日志文件,与业务日志分离(见 logback.xml);
+ * 2、日志记录统一打印指令含义,格式:指令=中文含义(service_id);
+ * 3、心跳指令(上行 ping、下行 pong)不记录,避免日志量过大。
+ */
+public class IotCommandLog {
+
+    /** 独立日志 logger,输出到独立日志文件(logback.xml 中 pile-command 配置) */
+    private static final Logger COMMAND_LOG = LoggerFactory.getLogger("pile-command");
+
+    /** 上行指令 service_id -> 中文含义 */
+    private static final Map<String, String> UPLINK_MEANING = new HashMap<>();
+
+    static {
+        UPLINK_MEANING.put(SendTagConstant.ONLINE, "充电桩登录认证");
+        UPLINK_MEANING.put(SendTagConstant.PING, "充电桩心跳包");
+        UPLINK_MEANING.put(SendTagConstant.END_CHARGE, "充电结束");
+        UPLINK_MEANING.put(SendTagConstant.ERROR_MESSAGE, "错误报文");
+        UPLINK_MEANING.put(SendTagConstant.BILLING_MODE_VERIFY, "计费模型验证请求");
+        UPLINK_MEANING.put(SendTagConstant.ACQUISITION_BILLING_MODE, "充电桩计费模型请求");
+        UPLINK_MEANING.put(SendTagConstant.UPLOAD_REAL_TIME_MONITORING_DATA, "上传实时监测数据");
+        UPLINK_MEANING.put(SendTagConstant.CHARGING_HANDSHAKE, "充电握手");
+        UPLINK_MEANING.put(SendTagConstant.PARAMETER_SETTING, "参数配置");
+        UPLINK_MEANING.put(SendTagConstant.BMS_ABORT, "充电阶段BMS中止");
+        UPLINK_MEANING.put(SendTagConstant.MOTOR_ABORT, "充电阶段充电机中止");
+        UPLINK_MEANING.put(SendTagConstant.BMS_DEMAND_AND_CHARGER_EXPORTATION, "充电过程BMS需求、充电机输出");
+        UPLINK_MEANING.put(SendTagConstant.BMS_INFORMATION, "充电过程BMS信息");
+        UPLINK_MEANING.put(SendTagConstant.CHARGING_PILE_STARTS_CHARGING, "充电桩主动申请启动充电");
+        UPLINK_MEANING.put(SendTagConstant.PLATFORM_START_CHARGING_REPLY, "远程启机命令回复");
+        UPLINK_MEANING.put(SendTagConstant.PLATFORM_STOP_CHARGING_REPLY, "远程停机命令回复");
+        UPLINK_MEANING.put(SendTagConstant.TRANSACTION_RECORD, "交易记录");
+        UPLINK_MEANING.put(SendTagConstant.UPDATE_BALANCE_REPLY, "余额更新应答");
+        UPLINK_MEANING.put(SendTagConstant.SYNCHRONIZE_OFFLINE_CARD_REPLY, "卡数据同步应答");
+        UPLINK_MEANING.put(SendTagConstant.CLEAR_OFFLINE_CARD_REPLY, "离线卡数据清除应答");
+        UPLINK_MEANING.put(SendTagConstant.QUERY_OFFLINE_CARD_REPLY, "离线卡数据查询应答");
+        UPLINK_MEANING.put(SendTagConstant.WORKING_PARAMETER_SETTING_REPLY, "充电桩工作参数设置应答");
+        UPLINK_MEANING.put(SendTagConstant.TIMING_SETTING, "对时设置");
+        UPLINK_MEANING.put(SendTagConstant.SETUP_BILLING_MODEL_REPLY, "计费模型应答");
+        UPLINK_MEANING.put(SendTagConstant.GROUND_LOCK_REAL_TIME_DATA, "地锁数据上送");
+        UPLINK_MEANING.put(SendTagConstant.CHARGING_PILE_RETURNS_GROUND_LOCK_DATA, "充电桩返回地锁数据");
+        UPLINK_MEANING.put(SendTagConstant.PLATFORM_RESTART_REPLY, "远程重启应答");
+        UPLINK_MEANING.put(SendTagConstant.PLATFORM_REMOTE_UPDATE_REPLY, "远程更新应答");
+        UPLINK_MEANING.put(SendTagConstant.QR_CODE_DELIVERY_REPLY, "二维码下发应答");
+        UPLINK_MEANING.put(SendTagConstant.SECURITY_DETECTION, "安全监测");
+        UPLINK_MEANING.put(SendTagConstant.GUN_STATUS, "推送充电设备接口状态信息");
+        UPLINK_MEANING.put(SendTagConstant.ORDER_STATUS, "推送充电状态信息");
+        UPLINK_MEANING.put(SendTagConstant.ORDER_INFO, "推送充电订单信息");
+    }
+
+    private IotCommandLog() {
+    }
+
+    /**
+     * 记录上行指令(设备 -> 平台)
+     * 心跳(ping)不记录
+     *
+     * @param deviceNo  设备编号
+     * @param serviceId 指令 service_id
+     * @param payload   报文内容
+     */
+    public static void uplink(String deviceNo, String serviceId, Object payload) {
+        if (SendTagConstant.PING.equals(serviceId)) {
+            // 心跳不记录日志
+            return;
+        }
+        COMMAND_LOG.info("[上行] 设备={} 指令={} 报文={}", deviceNo, describeUplink(serviceId), payload);
+    }
+
+    /**
+     * 记录下行指令(平台 -> 设备)
+     * 心跳应答(pong)不记录
+     *
+     * @param deviceNo  设备编号
+     * @param name      下发名称
+     * @param serviceId 指令 service_id
+     * @param payload   报文内容
+     */
+    public static void downlink(String deviceNo, String name, String serviceId, Object payload) {
+        if (isPong(serviceId)) {
+            // 心跳应答不记录日志
+            return;
+        }
+        COMMAND_LOG.info("[下行] 设备={} 指令={} 报文={}", deviceNo, describeDownlink(serviceId, name), payload);
+    }
+
+    /**
+     * 记录下行指令下发失败
+     * 心跳应答(pong)不记录
+     *
+     * @param deviceNo  设备编号
+     * @param name      下发名称
+     * @param serviceId 指令 service_id
+     * @param payload   报文内容
+     * @param error     失败原因
+     */
+    public static void downlinkFail(String deviceNo, String name, String serviceId, Object payload, String error) {
+        if (isPong(serviceId)) {
+            // 心跳应答不记录日志
+            return;
+        }
+        COMMAND_LOG.error("[下行失败] 设备={} 指令={} 报文={} 错误={}", deviceNo, describeDownlink(serviceId, name), payload, error);
+    }
+
+    /**
+     * 记录无法解析的上行原始报文(兜底)
+     * 报文解析失败时无法识别 service_id,不能判断是否为心跳,仍记录原始内容便于排查
+     *
+     * @param deviceNo   设备编号
+     * @param rawPayload 原始报文内容
+     */
+    public static void uplinkUnparsed(String deviceNo, Object rawPayload) {
+        COMMAND_LOG.info("[上行-原始报文] 设备={} 报文={}", deviceNo, rawPayload);
+    }
+
+    /** 是否心跳应答(pong) */
+    private static boolean isPong(String serviceId) {
+        return ServiceIdMenu.PONG.getValue().equals(serviceId);
+    }
+
+    /** 上行指令含义:中文含义(service_id) */
+    private static String describeUplink(String serviceId) {
+        String meaning = UPLINK_MEANING.get(serviceId);
+        if (meaning == null || meaning.isEmpty()) {
+            meaning = "未识别指令";
+        }
+        return meaning + "(" + serviceId + ")";
+    }
+
+    /** 下行指令含义:中文含义(service_id) */
+    private static String describeDownlink(String serviceId, String name) {
+        String meaning = null;
+        if (serviceId != null) {
+            for (ServiceIdMenu menu : ServiceIdMenu.values()) {
+                if (menu.getValue().equals(serviceId)) {
+                    meaning = menu.getKey();
+                    break;
+                }
+            }
+        }
+        if (meaning == null || meaning.isEmpty()) {
+            meaning = (name == null || name.isEmpty()) ? "未识别指令" : name;
+        }
+        return meaning + "(" + serviceId + ")";
+    }
+}
diff --git a/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml b/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
index bf1d78e..9c2b931 100644
--- a/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
+++ b/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
@@ -163,6 +163,32 @@
 		</filter>
 	</appender>
 	
+	<!-- 2.6 充电桩指令收发独立日志,独立文件输出(心跳不记录,见 IotCommandLog) -->
+	<appender name="PILE_COMMAND_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 正在记录的日志文档的路径及文档名 -->
+		<file>${log.path}/pile-command.log</file>
+		<!--日志文档输出格式-->
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+			<charset>UTF-8</charset> <!-- 设置字符集 -->
+		</encoder>
+		<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<!-- 日志归档 -->
+			<fileNamePattern>${log.path}/%d{yyyy-MM-dd}/pile-command-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>100MB</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!--日志文档保留天数-->
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+	</appender>
+
+	<!-- 充电桩指令收发日志:独立文件输出,不向 root 传递(additivity=false),避免污染业务日志 -->
+	<logger name="pile-command" level="INFO" additivity="false">
+		<appender-ref ref="PILE_COMMAND_FILE" />
+	</logger>
+
 	<!--
 		<logger>用来设置某一个包或者具体的某一个类的日志打印级别、
 		以及指定<appender>。<logger>仅有一个name属性,

--
Gitblit v1.7.1