fix: 物联网平台,amqp数据接入并插入TD数据库相关逻辑编码。
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
/**
|
||||
* AMQP 消息处理器接口
|
||||
*
|
||||
* 用于处理从阿里云 IoT 平台通过 AMQP 推送的设备消息
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
public interface AmqpMessageHandler {
|
||||
|
||||
/**
|
||||
* 处理接收到的 AMQP 消息
|
||||
*
|
||||
* @param message JSON 格式的消息内容
|
||||
*/
|
||||
void handleMessage(String message);
|
||||
|
||||
}
|
||||
@@ -39,4 +39,13 @@ public interface DeviceDataService {
|
||||
*/
|
||||
void unsubscribeDevice(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 保存设备数据(AMQP 推送的数据)
|
||||
*
|
||||
* @param deviceName 设备名称
|
||||
* @param productKey 产品Key
|
||||
* @param data 设备数据
|
||||
*/
|
||||
void saveDeviceData(String deviceName, String productKey, Object data);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.common.mybatis.core.page.PageQuery;
|
||||
import com.intc.common.mybatis.core.page.TableDataInfo;
|
||||
import com.intc.iot.domain.bo.DeviceRealtimeDataBo;
|
||||
import com.intc.iot.domain.vo.DeviceRealtimeDataVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备实时数据服务接口
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
public interface DeviceRealtimeDataService {
|
||||
|
||||
/**
|
||||
* 查询设备实时数据列表(分页)
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 设备实时数据列表
|
||||
*/
|
||||
TableDataInfo<DeviceRealtimeDataVo> queryPageList(DeviceRealtimeDataBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询设备实时数据列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 设备实时数据列表
|
||||
*/
|
||||
List<DeviceRealtimeDataVo> queryList(DeviceRealtimeDataBo bo);
|
||||
|
||||
/**
|
||||
* 根据ID查询设备实时数据
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @return 设备实时数据
|
||||
*/
|
||||
DeviceRealtimeDataVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询设备最新数据
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 最新数据
|
||||
*/
|
||||
DeviceRealtimeDataVo queryLatestData(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 查询设备最新属性数据
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 最新属性数据
|
||||
*/
|
||||
DeviceRealtimeDataVo queryLatestPropertyData(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 删除设备实时数据
|
||||
*
|
||||
* @param ids 主键ID数组
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean deleteByIds(Long[] ids);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.iot.domain.IotDeviceStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备状态服务接口
|
||||
* 对应 C# 的设备上下线状态处理
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
public interface DeviceStatusService {
|
||||
|
||||
/**
|
||||
* 处理设备状态变更事件
|
||||
* 对应 C# IOTTopicDeviceStatus 的处理
|
||||
*
|
||||
* @param statusData 状态数据
|
||||
*/
|
||||
void handleStatusChange(Map<String, Object> statusData);
|
||||
|
||||
/**
|
||||
* 查询设备当前状态
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 设备状态
|
||||
*/
|
||||
IotDeviceStatus queryDeviceStatus(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 查询设备是否在线
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return true-在线,false-离线
|
||||
*/
|
||||
boolean isDeviceOnline(String productKey, String deviceName);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 云端操作服务(Java 版等价封装)。
|
||||
*
|
||||
* 封装常用的设备查询、属性查询与属性下发能力,
|
||||
* 底层基于 {@link com.intc.iot.service.IotDeviceService} 和阿里云 IoT Java SDK。
|
||||
*/
|
||||
public interface IotCloudService {
|
||||
|
||||
// ================= 按 productKey + deviceName 操作 =================
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 查询设备详情。
|
||||
*/
|
||||
Map<String, Object> getDeviceInfo(String productKey, String deviceName) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 查询设备属性状态。
|
||||
*/
|
||||
Map<String, Object> getDeviceProperties(String productKey, String deviceName) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 设置设备属性。
|
||||
*/
|
||||
boolean setProperty(String productKey, String deviceName, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 调用设备服务。
|
||||
*/
|
||||
Map<String, Object> invokeService(String productKey, String deviceName, String identifier, String args) throws Exception;
|
||||
|
||||
// ================= 按 iotId 操作 =================
|
||||
|
||||
/**
|
||||
* 根据 iotId 查询设备详情。
|
||||
*/
|
||||
Map<String, Object> getDeviceInfo(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 iotId 查询设备属性状态。
|
||||
*/
|
||||
Map<String, Object> getDeviceProperties(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 iotId 设置设备属性。
|
||||
*/
|
||||
boolean setProperty(String iotId, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception;
|
||||
|
||||
/**
|
||||
* 调用设备服务(如自定义服务能力)。
|
||||
*/
|
||||
Map<String, Object> invokeService(String iotId, String identifier, String args) throws Exception;
|
||||
}
|
||||
@@ -12,12 +12,23 @@ public interface IotDeviceService {
|
||||
/**
|
||||
* 查询设备列表
|
||||
*
|
||||
* @param productKey 产品Key(可选)
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 设备列表
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
Map<String, Object> queryDeviceList(Integer pageNo, Integer pageSize) throws Exception;
|
||||
Map<String, Object> queryDeviceList(String productKey, Integer pageNo, Integer pageSize) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 ProductKey 和 DeviceName 查询设备信息
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 设备详情(包含 iotId 等)
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
Map<String, Object> findDeviceByProductKeyAndName(String productKey, String deviceName) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询设备详情
|
||||
@@ -67,4 +78,14 @@ public interface IotDeviceService {
|
||||
*/
|
||||
Map<String, Object> unbindDevice(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询设备物模型(模板)
|
||||
* 对应 C# IIOTCloudService.GetTemplate
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @return 物模型数据
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
Map<String, Object> queryThingModel(String productKey) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -34,4 +34,14 @@ public interface MqttService {
|
||||
*/
|
||||
void unsubscribe(String topic) throws Exception;
|
||||
|
||||
/**
|
||||
* 订阅设备状态 Topic
|
||||
* 对应 C# 的 /as/mqtt/status/ Topic 订阅
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
void subscribeDeviceStatus(String productKey, String deviceName) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
/**
|
||||
* VMS MNS 回执消费服务接口
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
public interface VmsMnsConsumerService {
|
||||
|
||||
/**
|
||||
* 启动 MNS 消费者
|
||||
*
|
||||
* @throws Exception 启动异常
|
||||
*/
|
||||
void start() throws Exception;
|
||||
|
||||
/**
|
||||
* 停止 MNS 消费者
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* 判断是否正在运行
|
||||
*
|
||||
* @return true: 运行中, false: 已停止
|
||||
*/
|
||||
boolean isRunning();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 语音电话通知服务(VMS)。
|
||||
*/
|
||||
public interface VmsNoticeService {
|
||||
|
||||
/**
|
||||
* 发送语音 TTS 呼叫(验证码或通知)。
|
||||
*
|
||||
* @param phoneNum 被叫手机号
|
||||
* @param params 模板参数键值对
|
||||
* @param outId 业务唯一标识(会在回执中返回)
|
||||
* @return 调用结果
|
||||
* @throws Exception 调用阿里云接口失败
|
||||
*/
|
||||
VmsNoticeResponse sendTtsCall(String phoneNum, Map<String, String> params, String outId) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
|
||||
/**
|
||||
* 告警电话通知服务接口
|
||||
* 对应 C# AlarmProcessor 中的 UpdateCallNoticeFromCallback 逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
public interface WarnCallNoticeService {
|
||||
|
||||
/**
|
||||
* 处理 VMS 回执,更新告警通知记录
|
||||
* 对应 C# ProcessVmsCallbacks + UpdateCallNoticeFromCallback
|
||||
*
|
||||
* @param callback VMS 回执
|
||||
* @return true: 处理成功, false: 未找到对应记录
|
||||
*/
|
||||
boolean processVmsCallback(VmsCallback callback);
|
||||
|
||||
/**
|
||||
* 批量处理未处理的 VMS 回执
|
||||
* 由定时任务调用
|
||||
*
|
||||
* @return 处理的回执数量
|
||||
*/
|
||||
int processUnhandledCallbacks();
|
||||
|
||||
/**
|
||||
* 移除指定设备的待通知记录
|
||||
* 当某个设备的语音通知成功后,移除该设备的其他待通知记录
|
||||
* 对应 C# RemovePendingNotificationsForDevice
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 移除的记录数量
|
||||
*/
|
||||
int removePendingNotificationsByDevice(Long deviceId);
|
||||
|
||||
/**
|
||||
* 清理超时的通知记录
|
||||
* 对应 C# CleanupExpiredNotifications
|
||||
*
|
||||
* 清理规则:
|
||||
* - 已呼叫成功(callStatus=1)但超过超时时间未收到回执
|
||||
* - 超时时间:3 分钟(与 C# 保持一致)
|
||||
*
|
||||
* @return 清理的记录数量
|
||||
*/
|
||||
int cleanupExpiredNotifications();
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.intc.iot.handler.DeviceDataHandler;
|
||||
import com.intc.iot.service.AmqpMessageHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* AMQP 消息处理器实现类
|
||||
*
|
||||
* 处理从阿里云 IoT 平台推送的设备消息,包括:
|
||||
* - 设备上报数据
|
||||
* - 设备上线/下线
|
||||
* - 设备状态变化
|
||||
* - 设备属性变化
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceDataHandler deviceDataHandler;
|
||||
|
||||
@Override
|
||||
public void handleMessage(String message) {
|
||||
try {
|
||||
JSONObject json = JSONUtil.parseObj(message);
|
||||
|
||||
// 检查是否包含 items 字段(设备属性数据)
|
||||
if (json.containsKey("items")) {
|
||||
// 直接处理设备属性数据
|
||||
handlePropertyMessage(json);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取消息类型
|
||||
String messageType = json.getStr("messageType");
|
||||
|
||||
if (messageType == null) {
|
||||
// 如果没有 messageType,可能是飞燕平台的原始消息格式
|
||||
// 尝试从 topic 或 method 判断消息类型
|
||||
String topic = json.getStr("topic");
|
||||
String method = json.getStr("method");
|
||||
|
||||
if (topic != null) {
|
||||
// 根据 topic 判断消息类型
|
||||
if (topic.contains("/property/post")) {
|
||||
handlePropertyMessageFromTopic(topic, message);
|
||||
} else if (topic.contains("/event/")) {
|
||||
handleEventMessageFromTopic(topic, message);
|
||||
} else {
|
||||
log.warn("未知的 Topic 格式: {}", topic);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (messageType) {
|
||||
case "property":
|
||||
// 设备属性上报
|
||||
handlePropertyMessage(json);
|
||||
break;
|
||||
case "event":
|
||||
// 设备事件上报
|
||||
handleEventMessage(json);
|
||||
break;
|
||||
case "status":
|
||||
// 设备状态变化
|
||||
handleStatusMessage(json);
|
||||
break;
|
||||
case "deviceLifeCycle":
|
||||
// 设备生命周期(上线/下线)
|
||||
handleLifeCycleMessage(json);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的消息类型: {}", messageType);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 AMQP 消息失败: {}", message, e);
|
||||
throw new RuntimeException("消息处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备属性上报消息
|
||||
*/
|
||||
private void handlePropertyMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String productKey = json.getStr("productKey");
|
||||
JSONObject items = json.getJSONObject("items");
|
||||
|
||||
// 如果没有 DeviceDataHandler,只记录日志
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过数据处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 DeviceDataHandler 处理数据(包括存储和报警)
|
||||
try {
|
||||
// 转换 items 格式:{"dissolvedOxygen": {"value": 7.82, "time": xxx}} -> {"dissolvedOxygen": 7.82}
|
||||
JSONObject params = new JSONObject();
|
||||
if (items != null) {
|
||||
for (String key : items.keySet()) {
|
||||
Object itemValue = items.get(key);
|
||||
if (itemValue instanceof JSONObject) {
|
||||
JSONObject itemObj = (JSONObject) itemValue;
|
||||
// 提取 value 字段
|
||||
if (itemObj.containsKey("value")) {
|
||||
// 字段名映射:currentTemperature -> temperature, dosat -> saturability
|
||||
String mappedKey = mapFieldName(key);
|
||||
params.set(mappedKey, itemObj.get("value"));
|
||||
}
|
||||
} else {
|
||||
// 如果不是嵌套格式,直接使用
|
||||
String mappedKey = mapFieldName(key);
|
||||
params.set(mappedKey, itemValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构造 topic 格式:/sys/{ProductKey}/{DeviceName}/thing/event/property/post
|
||||
String topic = String.format("/sys/%s/%s/thing/event/property/post", productKey, deviceName);
|
||||
// 构造飞燕平台消息格式
|
||||
JSONObject message = new JSONObject();
|
||||
message.set("method", "thing.event.property.post");
|
||||
message.set("params", params);
|
||||
|
||||
deviceDataHandler.handlePropertyPost(topic, message.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("使用 DeviceDataHandler 处理属性数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段名映射:将阿里云字段名映射为应用字段名
|
||||
*/
|
||||
private String mapFieldName(String fieldName) {
|
||||
switch (fieldName) {
|
||||
case "currentTemperature":
|
||||
return "temperature";
|
||||
case "dosat":
|
||||
return "saturability";
|
||||
case "Treference":
|
||||
return "treference";
|
||||
case "Tfluorescence":
|
||||
return "tfluorescence";
|
||||
case "Tcorrect":
|
||||
return "tcorrect";
|
||||
case "Tsignal":
|
||||
return "tsignal";
|
||||
case "salinitySet":
|
||||
return "salinity";
|
||||
default:
|
||||
// 其他字段名保持不变(转为小写)
|
||||
return fieldName.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从 Topic 判断的属性消息(飞燕平台原始格式)
|
||||
*/
|
||||
private void handlePropertyMessageFromTopic(String topic, String payload) {
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过数据处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deviceDataHandler.handlePropertyPost(topic, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("处理属性消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备事件消息
|
||||
*/
|
||||
private void handleEventMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String productKey = json.getStr("productKey");
|
||||
String identifier = json.getStr("identifier");
|
||||
JSONObject value = json.getJSONObject("value");
|
||||
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过事件处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造 topic 格式:/sys/{ProductKey}/{DeviceName}/thing/event/{EventIdentifier}/post
|
||||
String topic = String.format("/sys/%s/%s/thing/event/%s/post", productKey, deviceName, identifier);
|
||||
// 构造飞燕平台消息格式
|
||||
JSONObject message = new JSONObject();
|
||||
message.set("method", "thing.event." + identifier + ".post");
|
||||
message.set("params", value);
|
||||
|
||||
deviceDataHandler.handleEventPost(topic, message.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("处理设备事件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从 Topic 判断的事件消息(飞燕平台原始格式)
|
||||
*/
|
||||
private void handleEventMessageFromTopic(String topic, String payload) {
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过事件处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deviceDataHandler.handleEventPost(topic, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("处理事件消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备状态变化消息
|
||||
*/
|
||||
private void handleStatusMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String status = json.getStr("status");
|
||||
|
||||
log.debug("设备状态: {} - {}", deviceName, status);
|
||||
|
||||
// TODO: 更新设备状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备生命周期消息(上线/下线)
|
||||
*/
|
||||
private void handleLifeCycleMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String action = json.getStr("action");
|
||||
|
||||
log.debug("设备{}: {}", "online".equals(action) ? "上线" : "下线", deviceName);
|
||||
|
||||
// action: "online" 或 "offline"
|
||||
// TODO: 更新设备在线状态
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(MqttClient.class)
|
||||
@ConditionalOnBean(MqttClient.class)
|
||||
public class DeviceDataServiceImpl implements DeviceDataService {
|
||||
|
||||
@@ -44,24 +46,14 @@ public class DeviceDataServiceImpl implements DeviceDataService {
|
||||
public void subscribeDeviceProperties(String iotId) throws Exception {
|
||||
// 注意:需要根据 iotId 获取 productKey 和 deviceName
|
||||
// 这里简化处理,实际应该从数据库或缓存中查询
|
||||
log.info("订阅设备属性上报,IotId: {}", iotId);
|
||||
|
||||
// 示例:假设从设备信息中获取
|
||||
// String productKey = getProductKeyByIotId(iotId);
|
||||
// String deviceName = getDeviceNameByIotId(iotId);
|
||||
// String topic = String.format(PROPERTY_POST_TOPIC, productKey, deviceName);
|
||||
|
||||
// mqttService.subscribe(topic, 1);
|
||||
|
||||
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
log.warn("订阅设备属性上报功能未实现,请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
throw new UnsupportedOperationException("订阅设备属性功能未实现,请使用 subscribeAllDevices 方法");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeDeviceEvents(String iotId) throws Exception {
|
||||
log.info("订阅设备事件上报,IotId: {}", iotId);
|
||||
|
||||
// 类似属性订阅,需要映射关系
|
||||
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
log.warn("订阅设备事件上报功能未实现,请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
throw new UnsupportedOperationException("订阅设备事件功能未实现,请使用 subscribeAllDevices 方法");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,10 +68,22 @@ public class DeviceDataServiceImpl implements DeviceDataService {
|
||||
|
||||
@Override
|
||||
public void unsubscribeDevice(String iotId) throws Exception {
|
||||
log.info("取消订阅设备数据,IotId: {}", iotId);
|
||||
log.warn("取消订阅设备数据功能未实现,请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
throw new UnsupportedOperationException("取消订阅设备功能未实现");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDeviceData(String deviceName, String productKey, Object data) {
|
||||
log.info("保存设备数据 - 设备: {}, 产品: {}, 数据: {}", deviceName, productKey, data);
|
||||
|
||||
// 需要取消对应的 Topic 订阅
|
||||
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
// TODO: 实现数据保存逻辑
|
||||
// 1. 保存到 MySQL/PostgreSQL(结构化数据)
|
||||
// 2. 保存到 TDengine(时序数据)
|
||||
// 3. 触发告警逻辑
|
||||
// 4. 发送通知
|
||||
|
||||
// 示例:保存到实时数据表
|
||||
// deviceRealtimeDataService.save(deviceName, productKey, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.intc.common.core.utils.MapstructUtils;
|
||||
import com.intc.common.mybatis.core.page.PageQuery;
|
||||
import com.intc.common.mybatis.core.page.TableDataInfo;
|
||||
import com.intc.iot.domain.DeviceRealtimeData;
|
||||
import com.intc.iot.domain.bo.DeviceRealtimeDataBo;
|
||||
import com.intc.iot.domain.vo.DeviceRealtimeDataVo;
|
||||
import com.intc.iot.mapper.DeviceRealtimeDataMapper;
|
||||
import com.intc.iot.service.DeviceRealtimeDataService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备实时数据服务实现
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceRealtimeDataServiceImpl implements DeviceRealtimeDataService {
|
||||
|
||||
private final DeviceRealtimeDataMapper deviceRealtimeDataMapper;
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
private LambdaQueryWrapper<DeviceRealtimeData> buildQueryWrapper(DeviceRealtimeDataBo bo) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(ObjectUtil.isNotEmpty(bo.getProductKey()), DeviceRealtimeData::getProductKey, bo.getProductKey())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getDeviceName()), DeviceRealtimeData::getDeviceName, bo.getDeviceName())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getIotId()), DeviceRealtimeData::getIotId, bo.getIotId())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getDataType()), DeviceRealtimeData::getDataType, bo.getDataType())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getIdentifier()), DeviceRealtimeData::getIdentifier, bo.getIdentifier())
|
||||
.ge(ObjectUtil.isNotEmpty(bo.getStartTime()), DeviceRealtimeData::getReportTime, bo.getStartTime())
|
||||
.le(ObjectUtil.isNotEmpty(bo.getEndTime()), DeviceRealtimeData::getReportTime, bo.getEndTime())
|
||||
.orderByDesc(DeviceRealtimeData::getReportTime);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<DeviceRealtimeDataVo> queryPageList(DeviceRealtimeDataBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = buildQueryWrapper(bo);
|
||||
Page<DeviceRealtimeDataVo> page = deviceRealtimeDataMapper.selectVoPage(pageQuery.build(), wrapper);
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceRealtimeDataVo> queryList(DeviceRealtimeDataBo bo) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = buildQueryWrapper(bo);
|
||||
return deviceRealtimeDataMapper.selectVoList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRealtimeDataVo queryById(Long id) {
|
||||
return deviceRealtimeDataMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRealtimeDataVo queryLatestData(String productKey, String deviceName) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(DeviceRealtimeData::getProductKey, productKey)
|
||||
.eq(DeviceRealtimeData::getDeviceName, deviceName)
|
||||
.orderByDesc(DeviceRealtimeData::getReportTime)
|
||||
.last("LIMIT 1");
|
||||
return deviceRealtimeDataMapper.selectVoOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRealtimeDataVo queryLatestPropertyData(String productKey, String deviceName) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(DeviceRealtimeData::getProductKey, productKey)
|
||||
.eq(DeviceRealtimeData::getDeviceName, deviceName)
|
||||
.eq(DeviceRealtimeData::getDataType, "property")
|
||||
.orderByDesc(DeviceRealtimeData::getReportTime)
|
||||
.last("LIMIT 1");
|
||||
return deviceRealtimeDataMapper.selectVoOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteByIds(Long[] ids) {
|
||||
int rows = deviceRealtimeDataMapper.deleteBatchIds(Arrays.asList(ids));
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.intc.iot.domain.IotDeviceStatus;
|
||||
import com.intc.iot.mapper.IotDeviceStatusMapper;
|
||||
import com.intc.iot.service.DeviceStatusService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备状态服务实现
|
||||
* 对应 C# IOTTopicDeviceStatus 的处理逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(IotDeviceStatusMapper.class)
|
||||
@Slf4j
|
||||
public class DeviceStatusServiceImpl implements DeviceStatusService {
|
||||
|
||||
private final IotDeviceStatusMapper deviceStatusMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handleStatusChange(Map<String, Object> statusData) {
|
||||
try {
|
||||
String productKey = (String) statusData.get("productKey");
|
||||
String deviceName = (String) statusData.get("deviceName");
|
||||
String status = (String) statusData.get("status");
|
||||
Long statusTime = getLongValue(statusData, "statusTime");
|
||||
Long lastTime = getLongValue(statusData, "lastTime");
|
||||
String clientIp = (String) statusData.get("clientIp");
|
||||
|
||||
if (productKey == null || deviceName == null || status == null) {
|
||||
log.warn("[设备状态] 状态数据不完整,跳过处理: {}", JSONUtil.toJsonStr(statusData));
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[设备状态] 设备状态变更 - ProductKey: {}, DeviceName: {}, Status: {}",
|
||||
productKey, deviceName, status);
|
||||
|
||||
// 查询是否已存在记录
|
||||
IotDeviceStatus existingStatus = deviceStatusMapper.selectOne(
|
||||
new LambdaQueryWrapper<IotDeviceStatus>()
|
||||
.eq(IotDeviceStatus::getProductKey, productKey)
|
||||
.eq(IotDeviceStatus::getDeviceName, deviceName)
|
||||
);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (existingStatus != null) {
|
||||
// 更新现有记录
|
||||
existingStatus.setStatus(status);
|
||||
existingStatus.setStatusTime(statusTime);
|
||||
existingStatus.setLastTime(lastTime);
|
||||
existingStatus.setClientIp(clientIp);
|
||||
existingStatus.setUpdatedTime(now);
|
||||
deviceStatusMapper.updateById(existingStatus);
|
||||
|
||||
log.debug("[设备状态] 更新设备状态记录 - ID: {}, Status: {}", existingStatus.getId(), status);
|
||||
} else {
|
||||
// 新增记录
|
||||
IotDeviceStatus newStatus = new IotDeviceStatus();
|
||||
newStatus.setProductKey(productKey);
|
||||
newStatus.setDeviceName(deviceName);
|
||||
newStatus.setIotId((String) statusData.get("iotId"));
|
||||
newStatus.setStatus(status);
|
||||
newStatus.setStatusTime(statusTime);
|
||||
newStatus.setLastTime(lastTime);
|
||||
newStatus.setClientIp(clientIp);
|
||||
newStatus.setCreatedTime(now);
|
||||
newStatus.setUpdatedTime(now);
|
||||
deviceStatusMapper.insert(newStatus);
|
||||
|
||||
log.info("[设备状态] 新增设备状态记录 - ProductKey: {}, DeviceName: {}, Status: {}",
|
||||
productKey, deviceName, status);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[设备状态] 处理设备状态变更异常: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceStatus queryDeviceStatus(String productKey, String deviceName) {
|
||||
return deviceStatusMapper.selectOne(
|
||||
new LambdaQueryWrapper<IotDeviceStatus>()
|
||||
.eq(IotDeviceStatus::getProductKey, productKey)
|
||||
.eq(IotDeviceStatus::getDeviceName, deviceName)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeviceOnline(String productKey, String deviceName) {
|
||||
IotDeviceStatus status = queryDeviceStatus(productKey, deviceName);
|
||||
return status != null && "online".equalsIgnoreCase(status.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 Long 值
|
||||
*/
|
||||
private Long getLongValue(Map<String, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.intc.iot.service.IotCloudService;
|
||||
import com.intc.iot.service.IotDeviceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 云端操作服务实现。
|
||||
*
|
||||
* 基于 {@link IotDeviceService} 封装常用的设备查询、属性查询与属性下发能力,
|
||||
* 方便业务侧直接按 iotId 进行操作。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean({IAcsClient.class, IotDeviceService.class})
|
||||
public class IotCloudServiceImpl implements IotCloudService {
|
||||
|
||||
private final IotDeviceService iotDeviceService;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 解析 iotId。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String resolveIotId(String productKey, String deviceName) throws Exception {
|
||||
Map<String, Object> resp = iotDeviceService.findDeviceByProductKeyAndName(productKey, deviceName);
|
||||
Object successObj = resp.get("success");
|
||||
boolean success = successObj instanceof Boolean && (Boolean) successObj;
|
||||
if (!success) {
|
||||
log.warn("[IotCloudService] 根据 productKey={} deviceName={} 查询设备失败: {}", productKey, deviceName, resp);
|
||||
return null;
|
||||
}
|
||||
|
||||
Object dataObj = resp.get("data");
|
||||
if (dataObj == null) {
|
||||
log.warn("[IotCloudService] 查询设备返回数据为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理阿里云 SDK 返回的数据结构
|
||||
try {
|
||||
// 尝试获取 deviceList
|
||||
List<?> deviceList = null;
|
||||
if (dataObj instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
Object listObj = dataMap.get("deviceList");
|
||||
if (listObj instanceof List) {
|
||||
deviceList = (List<?>) listObj;
|
||||
}
|
||||
} else {
|
||||
// 如果 dataObj 本身有 getDeviceList 方法(通过反射调用)
|
||||
java.lang.reflect.Method method = dataObj.getClass().getMethod("getDeviceList");
|
||||
Object result = method.invoke(dataObj);
|
||||
if (result instanceof List) {
|
||||
deviceList = (List<?>) result;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceList == null || deviceList.isEmpty()) {
|
||||
log.warn("[IotCloudService] 未找到设备, productKey={}, deviceName={}", productKey, deviceName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取第一个设备的 iotId
|
||||
Object deviceObj = deviceList.get(0);
|
||||
if (deviceObj instanceof Map) {
|
||||
Map<String, Object> deviceMap = (Map<String, Object>) deviceObj;
|
||||
Object iotIdObj = deviceMap.get("iotId");
|
||||
return iotIdObj != null ? iotIdObj.toString() : null;
|
||||
} else {
|
||||
// 通过反射获取 iotId
|
||||
java.lang.reflect.Method method = deviceObj.getClass().getMethod("getIotId");
|
||||
Object iotIdObj = method.invoke(deviceObj);
|
||||
return iotIdObj != null ? iotIdObj.toString() : null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[IotCloudService] 解析设备信息失败, productKey={}, deviceName={}", productKey, deviceName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceInfo(String productKey, String deviceName) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return null;
|
||||
}
|
||||
return getDeviceInfo(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceProperties(String productKey, String deviceName) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return null;
|
||||
}
|
||||
return getDeviceProperties(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setProperty(String productKey, String deviceName, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return false;
|
||||
}
|
||||
return setProperty(iotId, properties, checkSuccess, retryCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> invokeService(String productKey, String deviceName, String identifier, String args) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return null;
|
||||
}
|
||||
return invokeService(iotId, identifier, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceInfo(String iotId) throws Exception {
|
||||
log.info("[IotCloudService] 查询设备详情, iotId={}", iotId);
|
||||
return iotDeviceService.queryDeviceInfo(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceProperties(String iotId) throws Exception {
|
||||
log.info("[IotCloudService] 查询设备属性, iotId={}", iotId);
|
||||
return iotDeviceService.queryDeviceProperties(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setProperty(String iotId, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception {
|
||||
log.info("[IotCloudService] 设置设备属性, iotId={}, properties={} ", iotId, properties);
|
||||
|
||||
// 将属性 Map 转为阿里云 SDK 需要的 JSON 字符串
|
||||
String itemsJson = JSONUtil.toJsonStr(properties);
|
||||
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
Map<String, Object> resp = iotDeviceService.setDeviceProperty(iotId, itemsJson);
|
||||
Object successObj = resp.get("success");
|
||||
boolean success = successObj instanceof Boolean && (Boolean) successObj;
|
||||
|
||||
if (!checkSuccess) {
|
||||
// 不强制校验 success,直接按照返回值判断即可
|
||||
return success;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
attempt++;
|
||||
if (attempt > retryCount) {
|
||||
log.warn("[IotCloudService] 设置设备属性失败且重试次数耗尽, iotId={}, properties={}, resp={}", iotId, properties, resp);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.warn("[IotCloudService] 设置设备属性失败, 准备重试 {}/{} 次, iotId={}, properties={}, resp={}", attempt, retryCount, iotId, properties, resp);
|
||||
try {
|
||||
Thread.sleep(500L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> invokeService(String iotId, String identifier, String args) throws Exception {
|
||||
log.info("[IotCloudService] 调用设备服务, iotId={}, identifier={} ", iotId, identifier);
|
||||
return iotDeviceService.invokeService(iotId, identifier, args);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.intc.iot.service.IotDeviceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -20,6 +21,7 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(IAcsClient.class)
|
||||
@ConditionalOnBean(IAcsClient.class)
|
||||
public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
|
||||
@@ -27,15 +29,27 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
private final AliyunIotProperties iotProperties;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryDeviceList(Integer pageNo, Integer pageSize) throws Exception {
|
||||
log.info("查询设备列表,页码: {}, 每页大小: {}", pageNo, pageSize);
|
||||
public Map<String, Object> queryDeviceList(String productKey, Integer pageNo, Integer pageSize) throws Exception {
|
||||
log.info("查询设备列表,ProductKey: {}, 页码: {}, 每页大小: {}", productKey, pageNo, pageSize);
|
||||
|
||||
QueryDeviceRequest request = new QueryDeviceRequest();
|
||||
// ProductKey 是必填参数,如果为空则报错
|
||||
if (productKey == null || productKey.isEmpty()) {
|
||||
log.error("查询设备列表失败:ProductKey 不能为空");
|
||||
throw new IllegalArgumentException("ProductKey is mandatory for this action");
|
||||
}
|
||||
request.setProductKey(productKey);
|
||||
request.setCurrentPage(pageNo);
|
||||
request.setPageSize(pageSize);
|
||||
|
||||
log.info("调用阿里云 API,请求参数: ProductKey={}, CurrentPage={}, PageSize={}",
|
||||
request.getProductKey(), request.getCurrentPage(), request.getPageSize());
|
||||
|
||||
QueryDeviceResponse response = acsClient.getAcsResponse(request);
|
||||
|
||||
log.info("阿里云 API 返回: Success={}, ErrorMessage={}",
|
||||
response.getSuccess(), response.getErrorMessage());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", response.getSuccess());
|
||||
result.put("data", response.getData());
|
||||
@@ -43,6 +57,38 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findDeviceByProductKeyAndName(String productKey, String deviceName) throws Exception {
|
||||
log.info("根据 ProductKey 和 DeviceName 查询设备信息,ProductKey: {}, DeviceName: {}", productKey, deviceName);
|
||||
|
||||
// 使用 QueryDeviceDetail API 查询特定设备
|
||||
QueryDeviceDetailRequest request = new QueryDeviceDetailRequest();
|
||||
request.setProductKey(productKey);
|
||||
request.setDeviceName(deviceName);
|
||||
|
||||
QueryDeviceDetailResponse response = acsClient.getAcsResponse(request);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", response.getSuccess());
|
||||
|
||||
// 将设备详情数据包装成类似 QueryDevice 的格式,以保持接口兼容性
|
||||
if (response.getSuccess() && response.getData() != null) {
|
||||
// 创建一个设备列表,包含查询到的单个设备
|
||||
Map<String, Object> dataWrapper = new HashMap<>();
|
||||
java.util.List<Object> deviceList = new java.util.ArrayList<>();
|
||||
deviceList.add(response.getData());
|
||||
dataWrapper.put("deviceList", deviceList);
|
||||
result.put("data", dataWrapper);
|
||||
result.put("total", 1);
|
||||
} else {
|
||||
result.put("data", null);
|
||||
result.put("total", 0);
|
||||
result.put("errorMessage", response.getErrorMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryDeviceInfo(String iotId) throws Exception {
|
||||
log.info("查询设备详情,IotId: {}", iotId);
|
||||
@@ -122,4 +168,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryThingModel(String productKey) throws Exception {
|
||||
log.info("查询设备物模型,ProductKey: {}", productKey);
|
||||
|
||||
QueryThingModelRequest request = new QueryThingModelRequest();
|
||||
request.setProductKey(productKey);
|
||||
|
||||
QueryThingModelResponse response = acsClient.getAcsResponse(request);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", response.getSuccess());
|
||||
result.put("data", response.getData());
|
||||
result.put("errorMessage", response.getErrorMessage());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import com.intc.iot.handler.DeviceDataHandler;
|
||||
import com.intc.iot.service.DeviceStatusService;
|
||||
import com.intc.iot.service.MqttService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -9,6 +10,7 @@ import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -19,6 +21,7 @@ import org.springframework.stereotype.Service;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(MqttClient.class)
|
||||
@ConditionalOnBean(MqttClient.class)
|
||||
public class MqttServiceImpl implements MqttService {
|
||||
|
||||
@@ -27,6 +30,9 @@ public class MqttServiceImpl implements MqttService {
|
||||
@Autowired(required = false)
|
||||
private DeviceDataHandler deviceDataHandler;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceStatusService deviceStatusService;
|
||||
|
||||
@Override
|
||||
public void publish(String topic, String payload, int qos) throws Exception {
|
||||
if (!mqttClient.isConnected()) {
|
||||
@@ -67,6 +73,11 @@ public class MqttServiceImpl implements MqttService {
|
||||
log.debug("未匹配的 Topic 类型: {}", topic);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理设备状态变更(对应 C# 的 /as/mqtt/status/ Topic)
|
||||
if (deviceStatusService != null && topic.contains("/mqtt/status/")) {
|
||||
handleDeviceStatus(topic, payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -84,4 +95,52 @@ public class MqttServiceImpl implements MqttService {
|
||||
log.info("MQTT 主题取消订阅成功,Topic: {}", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备状态变更
|
||||
* 对应 C# IOTTopicDeviceStatus 的处理逻辑
|
||||
*
|
||||
* @param topic Topic
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
private void handleDeviceStatus(String topic, String payload) {
|
||||
try {
|
||||
log.debug("[设备状态] 收到状态变更消息 - Topic: {}", topic);
|
||||
|
||||
// 解析 JSON 消息
|
||||
cn.hutool.json.JSONObject jsonObject = cn.hutool.json.JSONUtil.parseObj(payload);
|
||||
|
||||
// 从 Topic 中提取 productKey 和 deviceName
|
||||
// Topic 格式: /as/mqtt/status/{productKey}/{deviceName}
|
||||
String[] parts = topic.split("/");
|
||||
if (parts.length >= 6) { // 需要至少 6 个元素
|
||||
String productKey = parts[4];
|
||||
String deviceName = parts[5];
|
||||
|
||||
java.util.Map<String, Object> statusData = new java.util.HashMap<>();
|
||||
statusData.put("productKey", productKey);
|
||||
statusData.put("deviceName", deviceName);
|
||||
statusData.put("status", jsonObject.getStr("status")); // online/offline
|
||||
statusData.put("statusTime", jsonObject.getLong("statusTime"));
|
||||
statusData.put("lastTime", jsonObject.getLong("lastTime"));
|
||||
statusData.put("clientIp", jsonObject.getStr("clientIp"));
|
||||
statusData.put("iotId", jsonObject.getStr("iotId"));
|
||||
|
||||
// 调用服务处理
|
||||
deviceStatusService.handleStatusChange(statusData);
|
||||
} else {
|
||||
log.warn("[设备状态] Topic 格式不正确: {}", topic);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[设备状态] 处理设备状态变更异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeDeviceStatus(String productKey, String deviceName) throws Exception {
|
||||
// 订阅设备状态 Topic(对应 C# 的 /as/mqtt/status/{productKey}/{deviceName})
|
||||
String statusTopic = String.format("/as/mqtt/status/%s/%s", productKey, deviceName);
|
||||
subscribe(statusTopic, 1);
|
||||
log.info("订阅设备状态 Topic 成功: {}", statusTopic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyun.mns.client.CloudAccount;
|
||||
import com.aliyun.mns.client.CloudQueue;
|
||||
import com.aliyun.mns.client.MNSClient;
|
||||
import com.aliyun.mns.model.Message;
|
||||
import com.intc.iot.config.AliyunIotProperties;
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
import com.intc.iot.mapper.VmsCallbackMapper;
|
||||
import com.intc.iot.service.VmsMnsConsumerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* VMS MNS 回执消费服务实现
|
||||
* 注意:直接使用 AccessKey/Secret 访问 MNS,不再依赖 dybaseapi
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "aliyun.living-iot.vms", name = "mns-enabled", havingValue = "true")
|
||||
@ConditionalOnBean(AliyunIotProperties.class)
|
||||
@Slf4j
|
||||
public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
|
||||
private final AliyunIotProperties aliyunIotProperties;
|
||||
private final VmsCallbackMapper vmsCallbackMapper;
|
||||
|
||||
/**
|
||||
* MNS 配置常量
|
||||
* 注意:这些值需要根据你的实际阿里云账户配置
|
||||
*/
|
||||
private static final String VMS_MNS_QUEUE_NAME = "Alicom-Queue-1572610294777992-VoiceReport";
|
||||
private static final String MNS_ACCOUNT_ENDPOINT = "https://1943695596114318.mns.cn-hangzhou.aliyuncs.com/";
|
||||
private static final int MNS_THREAD_COUNT = 2;
|
||||
private static final int POLL_WAIT_SECONDS = 30;
|
||||
|
||||
/**
|
||||
* 应用标识(用于过滤 out_id)
|
||||
*/
|
||||
private String appName;
|
||||
|
||||
/**
|
||||
* MNS 客户端
|
||||
*/
|
||||
private MNSClient mnsClient;
|
||||
|
||||
/**
|
||||
* 线程池
|
||||
*/
|
||||
private ExecutorService executorService;
|
||||
|
||||
/**
|
||||
* 运行状态标志
|
||||
*/
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
if (running.compareAndSet(false, true)) {
|
||||
log.info("启动 VMS MNS 回执消费服务...");
|
||||
|
||||
// 从配置读取应用名称
|
||||
appName = aliyunIotProperties.getAppKey() != null && !aliyunIotProperties.getAppKey().isEmpty()
|
||||
? aliyunIotProperties.getAppKey()
|
||||
: "fishery-backend";
|
||||
log.info("VMS 回执过滤应用名称: {}", appName);
|
||||
|
||||
// 直接使用 AccessKey/Secret 创建 MNS 客户端
|
||||
CloudAccount account = new CloudAccount(
|
||||
aliyunIotProperties.getAccessKeyId(),
|
||||
aliyunIotProperties.getAccessKeySecret(),
|
||||
MNS_ACCOUNT_ENDPOINT
|
||||
);
|
||||
mnsClient = account.getMNSClient();
|
||||
log.info("MNS 客户端初始化成功,Endpoint: {}", MNS_ACCOUNT_ENDPOINT);
|
||||
|
||||
// 创建线程池
|
||||
executorService = Executors.newFixedThreadPool(MNS_THREAD_COUNT);
|
||||
|
||||
// 启动消费线程(对应 C# 的 MNSHandle 循环)
|
||||
for (int i = 0; i < MNS_THREAD_COUNT; i++) {
|
||||
executorService.submit(this::mnsConsumeLoop);
|
||||
}
|
||||
|
||||
log.info("VMS MNS 回执消费服务启动成功,线程数: {}", MNS_THREAD_COUNT);
|
||||
} else {
|
||||
log.warn("VMS MNS 回执消费服务已在运行中,无需重复启动");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (running.compareAndSet(true, false)) {
|
||||
log.info("停止 VMS MNS 回执消费服务...");
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 MNS 客户端
|
||||
if (mnsClient != null) {
|
||||
mnsClient.close();
|
||||
}
|
||||
|
||||
log.info("VMS MNS 回执消费服务已停止");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* MNS 消费循环
|
||||
*/
|
||||
private void mnsConsumeLoop() {
|
||||
log.info("MNS 消费线程 {} 启动", Thread.currentThread().getName());
|
||||
|
||||
CloudQueue queue = null;
|
||||
try {
|
||||
// 获取队列引用
|
||||
queue = mnsClient.getQueueRef(VMS_MNS_QUEUE_NAME);
|
||||
log.info("获取 MNS Queue 成功: {}", VMS_MNS_QUEUE_NAME);
|
||||
} catch (Exception e) {
|
||||
log.error("获取 MNS Queue 失败: {}", e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
while (running.get()) {
|
||||
try {
|
||||
// 批量接收消息
|
||||
List<Message> messages = queue.batchPopMessage(16, POLL_WAIT_SECONDS);
|
||||
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debug("接收到 {} 条 VMS 回执消息", messages.size());
|
||||
|
||||
// 处理消息
|
||||
for (Message message : messages) {
|
||||
try {
|
||||
processMessage(message, queue);
|
||||
} catch (Exception e) {
|
||||
log.error("处理 VMS 回执消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 检查是否为中断异常
|
||||
if (e instanceof InterruptedException || e.getCause() instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.info("MNS 消费线程被中断");
|
||||
break;
|
||||
}
|
||||
|
||||
log.error("MNS 消费循环异常: {}", e.getMessage(), e);
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("MNS 消费线程 {} 退出", Thread.currentThread().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单条消息
|
||||
*
|
||||
* @param message MNS 消息
|
||||
* @param queue MNS 队列
|
||||
*/
|
||||
private void processMessage(Message message, CloudQueue queue) {
|
||||
try {
|
||||
// 解码消息体(对应 C# 的 Base64 解码)
|
||||
String body = message.getMessageBodyAsString();
|
||||
byte[] decoded = Base64.getDecoder().decode(body);
|
||||
String jsonStr = new String(decoded, StandardCharsets.UTF_8);
|
||||
|
||||
log.debug("收到 VMS 回执消息: {}", jsonStr);
|
||||
|
||||
// 解析为实体(对应 C# 的 JSON.Deserialize<AliVmsCallBack>)
|
||||
Map<String, Object> callbackMap = JSONUtil.toBean(jsonStr, Map.class);
|
||||
String outId = (String) callbackMap.get("out_id");
|
||||
|
||||
// 过滤不属于当前系统的消息(对应 C# 的 out_id 判断)
|
||||
if (outId == null || outId.isEmpty()) {
|
||||
log.debug("跳过无效 out_id 的消息");
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 out_id 是否属于本系统(对应 C# 的 out_id.Equals(DefineValue.AppName))
|
||||
if (!outId.equals(appName)) {
|
||||
log.debug("收到语音通知回执不属于当前系统, outId={}, 期望={}", outId, appName);
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建实体
|
||||
VmsCallback vmsCallback = buildVmsCallback(callbackMap);
|
||||
|
||||
// 入库(对应 C# 的 CacheData.sListVmsCallback.Add)
|
||||
try {
|
||||
vmsCallbackMapper.insert(vmsCallback);
|
||||
log.info("VMS 回执入库成功 - CallId: {}, StatusCode: {}", vmsCallback.getCallId(), vmsCallback.getStatusCode());
|
||||
|
||||
// 成功处理后删除消息
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
} catch (org.springframework.dao.DuplicateKeyException e) {
|
||||
log.warn("VMS 回执已存在,跳过 - CallId: {}", vmsCallback.getCallId());
|
||||
// 重复数据也删除消息
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 VMS 回执消息异常: {}", e.getMessage(), e);
|
||||
// 注意:不删除消息,让它重新回到队列,等待下次处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 VmsCallback 实体
|
||||
*
|
||||
* @param map 回执 Map
|
||||
* @return VmsCallback
|
||||
*/
|
||||
private VmsCallback buildVmsCallback(Map<String, Object> map) {
|
||||
VmsCallback callback = new VmsCallback();
|
||||
callback.setCallId((String) map.get("call_id"));
|
||||
callback.setOutId((String) map.get("out_id"));
|
||||
callback.setStatusCode((String) map.get("status_code"));
|
||||
callback.setStatusMsg((String) map.get("status_msg"));
|
||||
callback.setHangupDirection((String) map.get("hangup_direction"));
|
||||
callback.setCaller((String) map.get("caller"));
|
||||
callback.setVoiceType((String) map.get("voice_type"));
|
||||
|
||||
// 时间戳字段
|
||||
callback.setOriginateTime(getLongValue(map, "originate_time"));
|
||||
callback.setStartTime(getLongValue(map, "start_time"));
|
||||
callback.setEndTime(getLongValue(map, "end_time"));
|
||||
|
||||
// 整数字段
|
||||
callback.setRingTime(getIntValue(map, "ring_time"));
|
||||
callback.setDuration(getIntValue(map, "duration"));
|
||||
|
||||
callback.setTollType((String) map.get("toll_type"));
|
||||
callback.setProcessed(0); // 初始为未处理
|
||||
callback.setCreatedTime(LocalDateTime.now());
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 Long 值
|
||||
*/
|
||||
private Long getLongValue(Map<String, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 Integer 值
|
||||
*/
|
||||
private Integer getIntValue(Map<String, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyuncs.CommonRequest;
|
||||
import com.aliyuncs.CommonResponse;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.http.MethodType;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.intc.iot.config.AliyunIotProperties;
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
import com.intc.iot.service.VmsNoticeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 阿里云语音通知服务实现(基于稳定版 SDK)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(AliyunIotProperties.class)
|
||||
public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
|
||||
/** 默认主叫显号(可以根据实际情况调整或改为配置注入) */
|
||||
private static final String CALL_SHOW_NUMBER = "0571000013978";
|
||||
|
||||
/** 默认 TTS 模板编码(当前取 C# 版本中的正式模板编码,可按环境调整) */
|
||||
private static final String TTS_CODE = "TTS_299550017";
|
||||
|
||||
private final AliyunIotProperties aliyunIotProperties;
|
||||
|
||||
/**
|
||||
* VMS 客户端单例,避免重复创建
|
||||
*/
|
||||
private volatile IAcsClient vmsClient;
|
||||
|
||||
private IAcsClient getClient() {
|
||||
if (vmsClient == null) {
|
||||
synchronized (this) {
|
||||
if (vmsClient == null) {
|
||||
DefaultProfile profile = DefaultProfile.getProfile(
|
||||
"cn-hangzhou",
|
||||
aliyunIotProperties.getAccessKeyId(),
|
||||
aliyunIotProperties.getAccessKeySecret()
|
||||
);
|
||||
vmsClient = new DefaultAcsClient(profile);
|
||||
log.info("VMS 客户端初始化成功");
|
||||
}
|
||||
}
|
||||
}
|
||||
return vmsClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VmsNoticeResponse sendTtsCall(String phoneNum, Map<String, String> params, String outId) throws Exception {
|
||||
log.info("[VmsNoticeService] 发送语音通知, phoneNum={}, outId={}, params={}", phoneNum, outId, params);
|
||||
|
||||
IAcsClient client = getClient();
|
||||
|
||||
// 使用 CommonRequest 构建请求
|
||||
CommonRequest request = new CommonRequest();
|
||||
request.setSysMethod(MethodType.POST);
|
||||
request.setSysDomain("dyvmsapi.aliyuncs.com");
|
||||
request.setSysVersion("2017-05-25");
|
||||
request.setSysAction("SingleCallByTts");
|
||||
|
||||
// 设置请求参数
|
||||
request.putQueryParameter("CalledNumber", phoneNum);
|
||||
request.putQueryParameter("CalledShowNumber", CALL_SHOW_NUMBER);
|
||||
request.putQueryParameter("TtsCode", TTS_CODE);
|
||||
request.putQueryParameter("TtsParam", JSONUtil.toJsonStr(params));
|
||||
request.putQueryParameter("PlayTimes", "3");
|
||||
request.putQueryParameter("Volume", "100");
|
||||
request.putQueryParameter("Speed", "5");
|
||||
if (outId != null && !outId.isEmpty()) {
|
||||
request.putQueryParameter("OutId", outId);
|
||||
}
|
||||
|
||||
try {
|
||||
CommonResponse response = client.getCommonResponse(request);
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
log.error("[VmsNoticeService] 语音呼叫请求异常, response 为空");
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
result.setSuccess(false);
|
||||
result.setMessage("呼叫请求异常");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
String responseData = response.getData();
|
||||
cn.hutool.json.JSONObject jsonResponse = JSONUtil.parseObj(responseData);
|
||||
|
||||
String code = jsonResponse.getStr("Code");
|
||||
String message = jsonResponse.getStr("Message");
|
||||
String callId = jsonResponse.getStr("CallId");
|
||||
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
result.setCode(code);
|
||||
result.setMessage(message);
|
||||
result.setCallId(callId);
|
||||
result.setSuccess("OK".equalsIgnoreCase(code));
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
log.warn("[VmsNoticeService] 语音呼叫失败, phoneNum={}, code={}, message={}, callId={}",
|
||||
phoneNum, code, message, callId);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("[VmsNoticeService] 语音呼叫异常", e);
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
result.setSuccess(false);
|
||||
result.setMessage("调用异常: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.intc.iot.domain.AquWarnCallNotice;
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
import com.intc.iot.mapper.AquWarnCallNoticeMapper;
|
||||
import com.intc.iot.mapper.VmsCallbackMapper;
|
||||
import com.intc.iot.service.WarnCallNoticeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 告警电话通知服务实现
|
||||
* 对应 C# AlarmProcessor 中的告警通知处理逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean({AquWarnCallNoticeMapper.class, VmsCallbackMapper.class})
|
||||
@Slf4j
|
||||
public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
private final AquWarnCallNoticeMapper warnCallNoticeMapper;
|
||||
private final VmsCallbackMapper vmsCallbackMapper;
|
||||
|
||||
/**
|
||||
* 呼叫状态枚举(对应 C# EnumCallNoticeStatus)
|
||||
*/
|
||||
private static final int CALL_STATUS_NO_CALL = 0; // 未呼叫
|
||||
private static final int CALL_STATUS_CALL_SUCCESS = 1; // 呼叫成功,等待结果反馈
|
||||
private static final int CALL_STATUS_CALL_FAIL = 2; // 呼叫失败
|
||||
private static final int CALL_STATUS_CALLBACK_SUCCESS = 3; // 呼叫反馈成功
|
||||
private static final int CALL_STATUS_CALLBACK_FAIL = 4; // 呼叫反馈失败
|
||||
|
||||
/**
|
||||
* VMS 成功状态码
|
||||
*/
|
||||
private static final String VMS_STATUS_CODE_SUCCESS = "200000"; // 用户听完语音
|
||||
private static final String VMS_STATUS_CODE_HANGUP = "200001"; // 用户未听完挂断但已收听
|
||||
|
||||
/**
|
||||
* 回执超时时间(分钟)
|
||||
* 对应 C# CallNoticeMinuteInterval = 3
|
||||
*/
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 3;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean processVmsCallback(VmsCallback callback) {
|
||||
if (callback == null || callback.getCallId() == null) {
|
||||
log.warn("[告警通知] 回执数据为空或缺少 CallId");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 处理回执信息 - 呼叫ID: {}, 状态码: {}", callback.getCallId(), callback.getStatusCode());
|
||||
|
||||
// 根据 CallId 查找对应的通知记录
|
||||
AquWarnCallNotice callNotice = warnCallNoticeMapper.selectOne(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getCallId, callback.getCallId())
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS) // 只处理呼叫成功等待反馈的记录
|
||||
);
|
||||
|
||||
if (callNotice == null) {
|
||||
log.warn("[告警通知] 未找到对应的通知记录 - 呼叫ID: {}", callback.getCallId());
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 找到对应的通知记录 - 记录ID: {}, 设备ID: {}, 电话: {}",
|
||||
callNotice.getId(), callNotice.getDeviceId(), callNotice.getMobilePhone());
|
||||
|
||||
// 更新通知记录(对应 C# UpdateCallNoticeFromCallback)
|
||||
updateCallNoticeFromCallback(callNotice, callback);
|
||||
|
||||
// 标记回执为已处理
|
||||
callback.setProcessed(1);
|
||||
vmsCallbackMapper.updateById(callback);
|
||||
|
||||
log.info("[告警通知] 回执处理完成 - 呼叫ID: {}, 设备ID: {}, 状态: {}",
|
||||
callback.getCallId(), callNotice.getDeviceId(),
|
||||
callNotice.getCallStatus() == CALL_STATUS_CALLBACK_SUCCESS ? "成功" : "失败");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int processUnhandledCallbacks() {
|
||||
log.debug("[告警通知] 开始批量处理未处理的 VMS 回执");
|
||||
|
||||
// 查询所有未处理的回执
|
||||
List<VmsCallback> unhandledCallbacks = vmsCallbackMapper.selectList(
|
||||
new LambdaQueryWrapper<VmsCallback>()
|
||||
.eq(VmsCallback::getProcessed, 0)
|
||||
.orderByAsc(VmsCallback::getCreatedTime)
|
||||
);
|
||||
|
||||
if (unhandledCallbacks.isEmpty()) {
|
||||
log.debug("[告警通知] 没有未处理的回执");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("[告警通知] 发现 {} 条未处理的回执,开始处理", unhandledCallbacks.size());
|
||||
|
||||
int processedCount = 0;
|
||||
for (VmsCallback callback : unhandledCallbacks) {
|
||||
try {
|
||||
if (processVmsCallback(callback)) {
|
||||
processedCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[告警通知] 处理回执异常 - 呼叫ID: {}, 异常: {}", callback.getCallId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[告警通知] 批量处理完成 - 成功: {}, 总数: {}", processedCount, unhandledCallbacks.size());
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
|
||||
public int removePendingNotificationsByDevice(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 移除设备 {} 的待通知记录", deviceId);
|
||||
|
||||
// 删除该设备所有未呼叫的通知记录(对应 C# RemovePendingNotificationsForDevice)
|
||||
int count = warnCallNoticeMapper.delete(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getDeviceId, deviceId)
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_NO_CALL)
|
||||
);
|
||||
|
||||
if (count > 0) {
|
||||
log.info("[告警通知] 成功移除设备 {} 的 {} 条待通知记录", deviceId, count);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cleanupExpiredNotifications() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime timeoutThreshold = now.minusMinutes(CALLBACK_TIMEOUT_MINUTES);
|
||||
|
||||
log.debug("[告警通知] 开始清理超时通知数据 - 超时时间: {} 分钟", CALLBACK_TIMEOUT_MINUTES);
|
||||
|
||||
// 查询所有超时的通知记录
|
||||
// 条件:
|
||||
// 1. callStatus = 1(呼叫成功,等待结果反馈)
|
||||
// 2. callTime < 现在时间 - 3分钟
|
||||
List<AquWarnCallNotice> expiredNotices = warnCallNoticeMapper.selectList(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS)
|
||||
.lt(AquWarnCallNotice::getCallTime, timeoutThreshold)
|
||||
);
|
||||
|
||||
if (expiredNotices.isEmpty()) {
|
||||
log.debug("[告警通知] 无超时回执记录需要清理");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("[告警通知] 发现 {} 条超时回执记录,开始清理", expiredNotices.size());
|
||||
|
||||
int cleanedCount = 0;
|
||||
for (AquWarnCallNotice notice : expiredNotices) {
|
||||
try {
|
||||
log.warn("[告警通知] 电话通知回执超时 - 呼叫ID: {}, 电话: {}, 设备ID: {}, 超时时间: {}分钟",
|
||||
notice.getCallId(), notice.getMobilePhone(), notice.getDeviceId(), CALLBACK_TIMEOUT_MINUTES);
|
||||
|
||||
// 更新状态为回执失败
|
||||
notice.setCallStatus(CALL_STATUS_CALLBACK_FAIL);
|
||||
notice.setStatusMsg("回执超时,未在规定时间内收到回执");
|
||||
notice.setUpdatedTime(now);
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
|
||||
cleanedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("[告警通知] 清理超时记录异常 - 呼叫ID: {}, 异常: {}", notice.getCallId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
log.info("[告警通知] 清理了 {} 个超时的回执记录", cleanedCount);
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电话通知的回执信息
|
||||
* 对应 C# UpdateCallNoticeFromCallback
|
||||
*
|
||||
* @param callNotice 通知记录
|
||||
* @param callback VMS 回执
|
||||
*/
|
||||
private void updateCallNoticeFromCallback(AquWarnCallNotice callNotice, VmsCallback callback) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 更新回执详细信息(直接映射字段)
|
||||
callNotice.setStatusCode(callback.getStatusCode());
|
||||
callNotice.setHangupDirection(callback.getHangupDirection());
|
||||
callNotice.setCaller(callback.getCaller());
|
||||
callNotice.setRingTime(convertToString(callback.getRingTime()));
|
||||
callNotice.setDuration(convertToString(callback.getDuration()));
|
||||
callNotice.setVoiceType(callback.getVoiceType());
|
||||
callNotice.setOriginateTime(convertToString(callback.getOriginateTime()));
|
||||
callNotice.setStartTime(convertToString(callback.getStartTime()));
|
||||
callNotice.setEndTime(convertToString(callback.getEndTime()));
|
||||
callNotice.setStatusMsg(callback.getStatusMsg());
|
||||
callNotice.setOutId(callback.getOutId());
|
||||
callNotice.setTollType(callback.getTollType());
|
||||
callNotice.setUpdatedTime(now);
|
||||
|
||||
// 根据状态码判断通话结果(对应 C# 的状态码判断逻辑)
|
||||
if (VMS_STATUS_CODE_SUCCESS.equals(callback.getStatusCode()) ||
|
||||
VMS_STATUS_CODE_HANGUP.equals(callback.getStatusCode())) {
|
||||
// 用户听完语音或提前挂机但已收听
|
||||
callNotice.setCallStatus(CALL_STATUS_CALLBACK_SUCCESS);
|
||||
|
||||
log.info("[告警通知] 电话通知成功 - 设备ID: {}, 电话: {}, 状态码: {}",
|
||||
callNotice.getDeviceId(), callNotice.getMobilePhone(), callback.getStatusCode());
|
||||
|
||||
// 通话成功后,移除该设备的其他待通知记录
|
||||
// 注意:removePendingNotificationsByDevice 使用 REQUIRES_NEW 事务传播,独立于当前事务
|
||||
try {
|
||||
removePendingNotificationsByDevice(callNotice.getDeviceId());
|
||||
} catch (Exception e) {
|
||||
// 失败也不影响主事务
|
||||
log.warn("[告警通知] 移除待通知记录失败: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 通话失败
|
||||
callNotice.setCallStatus(CALL_STATUS_CALLBACK_FAIL);
|
||||
|
||||
log.warn("[告警通知] 电话通知失败 - 设备ID: {}, 电话: {}, 状态码: {}",
|
||||
callNotice.getDeviceId(), callNotice.getMobilePhone(), callback.getStatusCode());
|
||||
}
|
||||
|
||||
// 更新到数据库
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Long/Integer 转换为 String
|
||||
* C# 中这些字段都是 string 类型
|
||||
*
|
||||
* @param value 数值
|
||||
* @return 字符串
|
||||
*/
|
||||
private String convertToString(Object value) {
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user