diff --git a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/config/DeviceSyncConfig.java b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/config/DeviceSyncConfig.java
new file mode 100644
index 0000000..fec641d
--- /dev/null
+++ b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/config/DeviceSyncConfig.java
@@ -0,0 +1,92 @@
+package cc.iotkit.manager.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 设备同步配置(盐城设备台账同步、盐城/南京实时数据转发)
+ *
+ * 配置前缀:device-sync
+ *
+ * @author codex
+ */
+@Component
+@ConfigurationProperties(prefix = "device-sync")
+@Data
+public class DeviceSyncConfig {
+
+ /**
+ * 总开关,联调前保持 false
+ */
+ private boolean enable = false;
+
+ /**
+ * 失败重试次数
+ */
+ private int retry = 3;
+
+ /**
+ * 盐城 token 加密密钥(DES/CBC/PKCS5Padding,密钥需 8 字节,如 88888888)
+ */
+ private String encryptKey = "";
+
+ /**
+ * 对接厂商名称(盐城 addorEdit 的 dockingCompany)
+ */
+ private String dockingCompany = "";
+
+ /**
+ * 盐城设备新增/修改接口
+ */
+ private String addOrEditUrl = "";
+
+ /**
+ * 盐城设备删除接口
+ */
+ private String deleteUrl = "";
+
+ /**
+ * 盐城实时数据接口
+ */
+ private String yanchengRealtimeUrl = "";
+
+ /**
+ * 南京远传数据接口(测试环境:http://36.7.170.39:9880/crgas/cr/alarm/remoteTrans/receive)
+ */
+ private String nanjingReportUrl = "";
+
+ /**
+ * 参与同步的产品白名单(产品 key);为空表示不限制
+ */
+ private List syncProductKeys = new ArrayList<>();
+
+ /**
+ * 盐城实时数据转发开关
+ */
+ private boolean yanchengRealtimeEnable = false;
+
+ /**
+ * 南京数据转发开关
+ */
+ private boolean nanjingEnable = false;
+
+ /**
+ * 心跳推送周期(秒),默认 10 秒
+ */
+ private int heartbeatIntervalSeconds = 10;
+
+ /**
+ * 事件类消息去重窗口(秒),默认 5 秒
+ */
+ private int dedupSeconds = 5;
+
+ /**
+ * 盐城设备同步 deviceCode 是否使用设备名(IMEI),默认 true;
+ * false 时使用 deviceId
+ */
+ private boolean useDeviceNameAsCode = true;
+}
diff --git a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/event/DeviceSyncEvent.java b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/event/DeviceSyncEvent.java
new file mode 100644
index 0000000..e164584
--- /dev/null
+++ b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/event/DeviceSyncEvent.java
@@ -0,0 +1,29 @@
+package cc.iotkit.manager.event;
+
+import cc.iotkit.model.device.DeviceInfo;
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 设备信息同步事件
+ *
+ * 设备创建/修改/删除后发布,由外部对接监听器(如盐城设备台账同步)消费。
+ *
+ * @author codex
+ */
+@Getter
+public class DeviceSyncEvent extends ApplicationEvent {
+
+ /**
+ * 操作类型:create / update / delete
+ */
+ private final String operation;
+
+ private final DeviceInfo device;
+
+ public DeviceSyncEvent(Object source, String operation, DeviceInfo device) {
+ super(source);
+ this.operation = operation;
+ this.device = device;
+ }
+}
diff --git a/iot-module/iot-openapi/src/main/java/cc/iotkit/openapi/sync/RealtimeDataForwarder.java b/iot-module/iot-openapi/src/main/java/cc/iotkit/openapi/sync/RealtimeDataForwarder.java
new file mode 100644
index 0000000..913d544
--- /dev/null
+++ b/iot-module/iot-openapi/src/main/java/cc/iotkit/openapi/sync/RealtimeDataForwarder.java
@@ -0,0 +1,323 @@
+package cc.iotkit.openapi.sync;
+
+import cc.iotkit.common.constant.Constants;
+import cc.iotkit.common.thing.ThingModelMessage;
+import cc.iotkit.data.manager.IDeviceInfoData;
+import cc.iotkit.manager.config.DeviceSyncConfig;
+import cc.iotkit.model.device.DeviceInfo;
+import cc.iotkit.mq.ConsumerHandler;
+import cc.iotkit.mq.MqConsumer;
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 实时数据转发器
+ *
+ * 订阅物模型消息总线(THING_MODEL_MESSAGE_TOPIC),将设备上报/告警/故障/恢复/上下线
+ * 按协议翻译后推送至盐城 dockingData 与南京 deviceInfo/receive。
+ * 心跳按固定周期(默认 10 秒)定时推送,事件类消息即时推送并做简单去重。
+ *
+ * 默认开关关闭,联调确认细节后再开启。
+ *
+ * @author codex
+ */
+@Slf4j
+@Component
+public class RealtimeDataForwarder implements ConsumerHandler, Runnable {
+
+ @Autowired
+ @Qualifier("deviceInfoDataCache")
+ private IDeviceInfoData deviceInfoData;
+
+ @Autowired
+ private MqConsumer thingModelMessageConsumer;
+
+ @Autowired
+ private DeviceSyncConfig config;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final OkHttpClient httpClient = new OkHttpClient.Builder()
+ .connectTimeout(5, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .build();
+
+ /**
+ * 设备最近一次上报数据缓存:deviceId -> 数据
+ */
+ private final Map latestDataCache = new ConcurrentHashMap<>();
+
+ /**
+ * 事件去重:deviceName:dedupKey -> 上次推送时间
+ */
+ private final Map dedupCache = new ConcurrentHashMap<>();
+
+ /**
+ * 推送线程池:与消息总线消费解耦,避免阻塞规则引擎
+ */
+ private final ExecutorService pushPool = new ThreadPoolExecutor(2, 8, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(200), new ThreadPoolExecutor.CallerRunsPolicy());
+
+ private ScheduledExecutorService heartbeatScheduler;
+
+ @PostConstruct
+ public void init() {
+ thingModelMessageConsumer.consume(Constants.THING_MODEL_MESSAGE_TOPIC, this);
+ heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "realtime-data-heartbeat");
+ t.setDaemon(true);
+ return t;
+ });
+ int interval = Math.max(1, config.getHeartbeatIntervalSeconds());
+ heartbeatScheduler.scheduleAtFixedRate(this, interval, interval, TimeUnit.SECONDS);
+ log.info("实时数据转发器已启动:heartbeatInterval={}s, yanchengRealtime={}, nanjing={}",
+ interval, config.isYanchengRealtimeEnable(), config.isNanjingEnable());
+ }
+
+ @PreDestroy
+ public void destroy() {
+ if (heartbeatScheduler != null) {
+ heartbeatScheduler.shutdown();
+ }
+ pushPool.shutdown();
+ }
+
+ @Override
+ public void handler(ThingModelMessage msg) {
+ try {
+ if (!config.isYanchengRealtimeEnable() && !config.isNanjingEnable()) {
+ return;
+ }
+ if (msg == null || StringUtils.isBlank(msg.getDeviceName())) {
+ return;
+ }
+ DeviceInfo device = deviceInfoData.findByDeviceName(msg.getDeviceName());
+ if (device == null || !inSyncProducts(device.getProductKey())) {
+ return;
+ }
+
+ // 设备上下线
+ if (ThingModelMessage.TYPE_STATE.equals(msg.getType())) {
+ if (ThingModelMessage.ID_OFFLINE.equals(msg.getIdentifier())) {
+ latestDataCache.remove(device.getDeviceId());
+ pushOffline(device);
+ }
+ return;
+ }
+
+ // 仅处理属性上报
+ if (!ThingModelMessage.TYPE_PROPERTY.equals(msg.getType())
+ || !ThingModelMessage.ID_PROPERTY_REPORT.equals(msg.getIdentifier())) {
+ return;
+ }
+ if (!(msg.getData() instanceof Map)) {
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ Map data = (Map) msg.getData();
+ long time = msg.getTime() != null ? msg.getTime() : System.currentTimeMillis();
+ latestDataCache.put(device.getDeviceId(), new DeviceLatestData(time, data, device));
+
+ RealtimeProtocolMapper.ClassifiedEvent event = RealtimeProtocolMapper.classify(data);
+ if (event == null) {
+ return;
+ }
+ // 事件类消息去重,心跳由定时任务负责
+ if (!tryAcquireDedup(device.getDeviceName(), event.getDedupKey())) {
+ return;
+ }
+ pushEvent(event, device, data, time);
+ } catch (Throwable e) {
+ log.error("实时数据转发处理异常:deviceName={}",
+ msg != null ? msg.getDeviceName() : null, e);
+ }
+ }
+
+ /**
+ * 定时心跳:按固定周期扫描最近有上报的设备,推送盐城 102 / 南京 01
+ */
+ @Override
+ public void run() {
+ try {
+ if (!config.isYanchengRealtimeEnable() && !config.isNanjingEnable()) {
+ return;
+ }
+ long now = System.currentTimeMillis();
+ long staleAfter = Math.max(1, config.getHeartbeatIntervalSeconds()) * 3L * 1000L;
+ List staleKeys = new ArrayList<>();
+ for (Map.Entry entry : latestDataCache.entrySet()) {
+ DeviceLatestData latest = entry.getValue();
+ try {
+ if (now - latest.getTime() > staleAfter) {
+ staleKeys.add(entry.getKey());
+ continue;
+ }
+ RealtimeProtocolMapper.ClassifiedEvent event = buildHeartbeatEvent(latest.getData());
+ pushEvent(event, latest.getDevice(), latest.getData(), latest.getTime());
+ } catch (Exception e) {
+ log.error("定时心跳推送异常:deviceName={}", latest.getDevice().getDeviceName(), e);
+ }
+ }
+ staleKeys.forEach(latestDataCache::remove);
+ } catch (Throwable e) {
+ log.error("定时心跳推送异常", e);
+ }
+ }
+
+ private RealtimeProtocolMapper.ClassifiedEvent buildHeartbeatEvent(Map data) {
+ RealtimeProtocolMapper.ClassifiedEvent event = RealtimeProtocolMapper.classify(data);
+ if (event == null) {
+ event = new RealtimeProtocolMapper.ClassifiedEvent();
+ event.setNodeId(RealtimeProtocolMapper.resolveNodeId(data));
+ }
+ event.setKind(RealtimeProtocolMapper.EVENT_HEARTBEAT);
+ event.setGasSensorState(0);
+ event.setErrorCode(0);
+ event.setContent("正常");
+ event.setDedupKey("heartbeat");
+ return event;
+ }
+
+ private void pushOffline(DeviceInfo device) {
+ RealtimeProtocolMapper.ClassifiedEvent event = new RealtimeProtocolMapper.ClassifiedEvent();
+ event.setKind(RealtimeProtocolMapper.EVENT_OFFLINE);
+ event.setGasSensorState(0);
+ event.setErrorCode(10);
+ event.setContent("离线");
+ event.setDedupKey("offline");
+ if (!tryAcquireDedup(device.getDeviceName(), event.getDedupKey())) {
+ return;
+ }
+ pushEvent(event, device, null, System.currentTimeMillis());
+ }
+
+ private void pushEvent(RealtimeProtocolMapper.ClassifiedEvent event, DeviceInfo device,
+ Map data, long time) {
+ if (config.isYanchengRealtimeEnable()) {
+ Map ycBody = RealtimeProtocolMapper.buildYanchengBody(
+ event, device, data, time, config.getDockingCompany());
+ sendYancheng(ycBody, device.getDeviceName());
+ }
+ if (config.isNanjingEnable()) {
+ Map njBody = RealtimeProtocolMapper.buildNanjingBody(
+ event, device, data, time, config.getDockingCompany());
+ sendNanjing(njBody, device.getDeviceName());
+ }
+ }
+
+ private void sendYancheng(Map body, String deviceName) {
+ if (StringUtils.isBlank(config.getYanchengRealtimeUrl())) {
+ log.warn("盐城实时数据 URL 未配置,跳过:deviceName={}", deviceName);
+ return;
+ }
+ pushPool.submit(() -> {
+ int maxRetry = Math.max(1, config.getRetry());
+ for (int i = 0; i < maxRetry; i++) {
+ try {
+ Request request = new Request.Builder()
+ .url(config.getYanchengRealtimeUrl())
+ .post(RequestBody.create(objectMapper.writeValueAsString(body),
+ MediaType.parse("application/json; charset=utf-8")))
+ .build();
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("HTTP " + response.code());
+ }
+ }
+ log.info("盐城实时数据推送成功:deviceName={}, serviceType={}, body={}",
+ deviceName, body.get("serviceType"), objectMapper.writeValueAsString(body));
+ return;
+ } catch (Exception e) {
+ log.warn("盐城实时数据推送失败,第{}次:deviceName={}, error={}",
+ i + 1, deviceName, e.getMessage());
+ }
+ }
+ });
+ }
+
+ private void sendNanjing(Map body, String deviceName) {
+ if (StringUtils.isBlank(config.getNanjingReportUrl())) {
+ log.warn("南京数据上报 URL 未配置,跳过:deviceName={}", deviceName);
+ return;
+ }
+ pushPool.submit(() -> {
+ int maxRetry = Math.max(1, config.getRetry());
+ for (int i = 0; i < maxRetry; i++) {
+ try {
+ Request request = new Request.Builder()
+ .url(config.getNanjingReportUrl())
+ .post(RequestBody.create(objectMapper.writeValueAsString(body),
+ MediaType.parse("application/json; charset=utf-8")))
+ .build();
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("HTTP " + response.code());
+ }
+ }
+ log.info("南京数据上报推送成功:deviceName={}, messageType={}, body={}",
+ deviceName, body.get("messageType"), objectMapper.writeValueAsString(body));
+ return;
+ } catch (Exception e) {
+ log.warn("南京数据上报推送失败,第{}次:deviceName={}, error={}",
+ i + 1, deviceName, e.getMessage());
+ }
+ }
+ });
+ }
+
+ private boolean inSyncProducts(String productKey) {
+ if (ObjectUtil.isEmpty(config.getSyncProductKeys())) {
+ return true;
+ }
+ return config.getSyncProductKeys().contains(productKey);
+ }
+
+ private boolean tryAcquireDedup(String deviceName, String dedupKey) {
+ if (StringUtils.isBlank(dedupKey)) {
+ return true;
+ }
+ long now = System.currentTimeMillis();
+ long dedupMs = Math.max(1, config.getDedupSeconds()) * 1000L;
+ String key = deviceName + ":" + dedupKey;
+ Long last = dedupCache.get(key);
+ if (last != null && now - last < dedupMs) {
+ return false;
+ }
+ dedupCache.put(key, now);
+ return true;
+ }
+
+ @Data
+ @AllArgsConstructor
+ private static class DeviceLatestData {
+ private long time;
+ private Map data;
+ private DeviceInfo device;
+ }
+}
diff --git a/iot-module/iot-openapi/src/main/java/cc/iotkit/openapi/sync/RealtimeProtocolMapper.java b/iot-module/iot-openapi/src/main/java/cc/iotkit/openapi/sync/RealtimeProtocolMapper.java
new file mode 100644
index 0000000..41c24d3
--- /dev/null
+++ b/iot-module/iot-openapi/src/main/java/cc/iotkit/openapi/sync/RealtimeProtocolMapper.java
@@ -0,0 +1,263 @@
+package cc.iotkit.openapi.sync;
+
+import cc.iotkit.model.device.DeviceInfo;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Date;
+
+/**
+ * 实时数据协议映射器
+ *
+ * 将平台内部物模型消息(ThingModelMessage.data)分类并翻译成
+ * 盐城 dockingData / 南京 deviceInfo/receive 两家平台的报文。
+ *
+ * @author codex
+ */
+public class RealtimeProtocolMapper {
+
+ /**
+ * 事件分类
+ */
+ public static final String EVENT_HEARTBEAT = "heartbeat";
+ public static final String EVENT_ALARM = "alarm";
+ public static final String EVENT_FAULT = "fault";
+ public static final String EVENT_RECOVER = "recover";
+ public static final String EVENT_OFFLINE = "offline";
+
+ /**
+ * 盐城 serviceType
+ */
+ public static final int YANCHENG_TYPE_HEARTBEAT = 102;
+ public static final int YANCHENG_TYPE_ALARM = 1001;
+ public static final int YANCHENG_TYPE_FAULT = 1002;
+ public static final int YANCHENG_TYPE_RECOVER = 1003;
+
+ /**
+ * 南京 messageType
+ */
+ public static final String NANJING_TYPE_MONITOR = "01";
+ public static final String NANJING_TYPE_FAULT = "02";
+ public static final String NANJING_TYPE_ALARM = "03";
+
+ private RealtimeProtocolMapper() {
+ }
+
+ /**
+ * 根据解析器输出 data 分类事件。
+ * 正常上报(128/129/130) → 心跳;报警 → 告警;故障/断网 → 故障;恢复 → 恢复。
+ *
+ * @return 分类结果,无法识别返回 null
+ */
+ public static ClassifiedEvent classify(Map data) {
+ if (data == null || data.isEmpty()) {
+ return null;
+ }
+
+ Object eventTypeObj = data.get("eventType");
+ Object eventTypeValueObj = data.get("eventTypeValue");
+ String value = eventTypeValueObj != null ? eventTypeValueObj.toString() : "";
+ int eventType = eventTypeObj instanceof Number ? ((Number) eventTypeObj).intValue() : -1;
+
+ ClassifiedEvent event = new ClassifiedEvent();
+ event.setNodeId(resolveNodeId(data));
+ event.setContent(value);
+ event.setDedupKey(value);
+
+ // 正常数据上报(心跳/监测)
+ if (eventType == 128 || eventType == 129 || eventType == 130
+ || value.contains("正常数据上报")) {
+ event.setKind(EVENT_HEARTBEAT);
+ event.setGasSensorState(0);
+ event.setErrorCode(0);
+ return event;
+ }
+
+ // 浓度变化上报按监测数据处理
+ if (value.contains("浓度")) {
+ event.setKind(EVENT_HEARTBEAT);
+ event.setGasSensorState(0);
+ event.setErrorCode(0);
+ return event;
+ }
+
+ // 告警恢复 / 故障恢复
+ if (value.contains("恢复")) {
+ event.setKind(EVENT_RECOVER);
+ event.setGasSensorState(0);
+ event.setErrorCode(0);
+ return event;
+ }
+
+ // 故障(传感器故障 / 通讯断网 / 设备故障)
+ if (value.contains("故障") || value.contains("断网")) {
+ event.setKind(EVENT_FAULT);
+ event.setGasSensorState(0);
+ event.setErrorCode(5);
+ return event;
+ }
+
+ // 告警(首警/报警/动火离人报警等)
+ if (value.contains("报警") || value.contains("告警") || value.contains("首警")) {
+ event.setKind(EVENT_ALARM);
+ // 高限 → 2,低限/其他 → 1
+ event.setGasSensorState(value.contains("高") ? 2 : 1);
+ event.setErrorCode(0);
+ return event;
+ }
+
+ return null;
+ }
+
+ /**
+ * 组装盐城 dockingData 请求体。
+ */
+ public static Map buildYanchengBody(ClassifiedEvent event, DeviceInfo device,
+ Map data, long timestamp,
+ String manufacturer) {
+ Map payload = new LinkedHashMap<>();
+ payload.put("gas_sensor_state", event.getGasSensorState());
+ payload.put("error_code", event.getErrorCode());
+ String potency = resolvePotency(data);
+ if (potency != null) {
+ payload.put("gas_sample_value", potency);
+ }
+ if (StringUtils.isNotBlank(device.getSignalStrength())) {
+ payload.put("signal_power", device.getSignalStrength());
+ }
+ if (StringUtils.isNotBlank(manufacturer)) {
+ payload.put("manufacturer_name", manufacturer);
+ }
+ payload.put("IMEI", device.getDeviceName());
+
+ Map body = new LinkedHashMap<>();
+ body.put("IMEI", device.getDeviceName());
+ body.put("serviceType", resolveYanchengServiceType(event.getKind()));
+ body.put("timestamp", timestamp);
+ body.put("payload", payload);
+ return body;
+ }
+
+ /**
+ * 组装南京 deviceInfo/receive 请求体。
+ */
+ public static Map buildNanjingBody(ClassifiedEvent event, DeviceInfo device,
+ Map data, long timestamp,
+ String manufacturer) {
+ Map body = new LinkedHashMap<>();
+ body.put("manufacturer", manufacturer);
+ body.put("sendTime", formatTime(timestamp));
+
+ // hostNo/deviceNo:单点设备取 IMEI;多点设备 hostNo 取 IMEI、deviceNo 取节点编号
+ String imei = device.getDeviceName();
+ body.put("hostNo", imei);
+ body.put("deviceNo", StringUtils.isNotBlank(event.getNodeId()) ? event.getNodeId() : imei);
+
+ body.put("messageType", resolveNanjingMessageType(event.getKind()));
+
+ String potency = resolvePotency(data);
+ if (potency != null) {
+ body.put("potency", potency);
+ }
+ if (StringUtils.isNotBlank(event.getContent())) {
+ body.put("content", event.getContent());
+ }
+ if (isMonitorLike(event.getKind())) {
+ body.put("onlineStatus", "00");
+ }
+ if (EVENT_OFFLINE.equals(event.getKind())) {
+ body.put("onlineStatus", "01");
+ }
+ body.put("longitude", device.getLongitude());
+ body.put("latitude", device.getLatitude());
+ body.put("remark", event.getContent());
+ return body;
+ }
+
+ private static boolean isMonitorLike(String kind) {
+ return EVENT_HEARTBEAT.equals(kind) || EVENT_RECOVER.equals(kind);
+ }
+
+ private static int resolveYanchengServiceType(String kind) {
+ switch (kind) {
+ case EVENT_ALARM:
+ return YANCHENG_TYPE_ALARM;
+ case EVENT_FAULT:
+ case EVENT_OFFLINE:
+ return YANCHENG_TYPE_FAULT;
+ case EVENT_RECOVER:
+ return YANCHENG_TYPE_RECOVER;
+ default:
+ return YANCHENG_TYPE_HEARTBEAT;
+ }
+ }
+
+ private static String resolveNanjingMessageType(String kind) {
+ switch (kind) {
+ case EVENT_ALARM:
+ return NANJING_TYPE_ALARM;
+ case EVENT_FAULT:
+ case EVENT_OFFLINE:
+ return NANJING_TYPE_FAULT;
+ default:
+ return NANJING_TYPE_MONITOR;
+ }
+ }
+
+ /**
+ * 浓度值:优先取带节点后缀的 dataDetectorValue0,其次取 dataDetectorValue。
+ * 仅返回可解析为数值的浓度,避免把联动箱反馈状态等字符串误当浓度上报。
+ */
+ private static String resolvePotency(Map data) {
+ if (data == null) {
+ return null;
+ }
+ Object v = data.get("dataDetectorValue0");
+ if (v == null) {
+ v = data.get("dataDetectorValue");
+ }
+ if (v == null) {
+ return null;
+ }
+ try {
+ Double.parseDouble(v.toString());
+ return v.toString();
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 探头编号:优先 nodeId0,其次 nodeId。
+ */
+ public static String resolveNodeId(Map data) {
+ if (data == null) {
+ return null;
+ }
+ Object v = data.get("nodeId0");
+ if (v == null) {
+ v = data.get("nodeId");
+ }
+ return v != null ? v.toString() : null;
+ }
+
+ private static String formatTime(long timestamp) {
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(timestamp));
+ }
+
+ /**
+ * 事件分类结果
+ */
+ @Data
+ public static class ClassifiedEvent {
+ private String kind;
+ private int gasSensorState;
+ private int errorCode;
+ private String content;
+ private String nodeId;
+ private String dedupKey;
+ }
+}
diff --git a/iot-starter/src/main/resources/application-dev.yml b/iot-starter/src/main/resources/application-dev.yml
index 388d7ba..ba18a23 100644
--- a/iot-starter/src/main/resources/application-dev.yml
+++ b/iot-starter/src/main/resources/application-dev.yml
@@ -276,3 +276,35 @@ plugin:
mainPackage: cc.iotkit
pluginPath:
- ./data/plugins
+
+# 设备同步配置(盐城设备台账同步、盐城/南京实时数据转发)
+# 联调前保持 enable=false,避免误推数据
+device-sync:
+ enable: false
+ retry: 3
+ # DES/CBC/PKCS5Padding 加密密钥(需 8 字节,与甲方 DesUtil.java 一致)
+ encrypt-key: ""
+ # 对接厂商名称
+ docking-company: 天津费加罗电子
+ # 盐城-设备新增/修改
+ add-or-edit-url: http://gas.safeyancheng.cn:8888/ycxf/ycDevice/addorEdit
+ # 盐城-设备删除
+ delete-url: http://gas.safeyancheng.cn:8888/ycxf/ycDevice/deleteByCode
+ # 盐城-实时数据
+ yancheng-realtime-url: http://jiekou.safeyancheng.cn:8889/gas/dockingData
+ # 南京-远传数据(测试环境)
+ nanjing-report-url: http://36.7.170.39:9880/crgas/cr/alarm/remoteTrans/receive
+ # 参与同步的产品白名单(FIGARO 燃气报警器相关产品 key)
+ sync-product-keys:
+ - CEMpmANABN7Tt6Jh # 家用报警器
+ - XmXYxjzihseT76As # 独立式报警器
+ - bAASX8tBjYQjBGFP # 工商业控制器
+ - WfpZZFkMxxbGfRca # 动火离人
+ - MQFejp7cyDMH3enG # 联动控制箱
+ yancheng-realtime-enable: false
+ nanjing-enable: false
+ heartbeat-interval-seconds: 10
+ # 事件类消息去重窗口(秒)
+ dedup-seconds: 5
+ # 盐城 deviceCode 使用设备名(IMEI)
+ use-device-name-as-code: true