fix: 物联网平台,amqp数据接入并插入TD数据库相关逻辑编码。

This commit is contained in:
tianyongbao
2026-01-10 01:20:51 +08:00
parent 28c33874f0
commit 0167de4156
56 changed files with 4842 additions and 158 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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: 更新设备在线状态
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}